Skip to main content

ferritin_core/columns/
gpu.rs

1//! Framework-agnostic GPU upload descriptors.
2//!
3//! These types describe *what* to upload — element type, component count,
4//! stride, row count, and the bytes — without depending on wgpu, bevy, or any
5//! other GPU framework. A renderer maps a [`BufferLayout`] onto its own vertex
6//! format or storage-buffer binding and copies [`GpuBuffer::bytes`] verbatim.
7
8use super::buffer::{Column, ScalarType};
9use super::error::{BoundaryError, to_u32};
10use std::borrow::Cow;
11
12/// Memory layout of one GPU buffer.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub struct BufferLayout {
15    /// Scalar type of each component.
16    pub scalar: ScalarType,
17    /// Components per row (1 for scalar columns, 3 or 4 for positions, 2 for line indices).
18    pub components: u32,
19    /// Bytes between the start of consecutive rows.
20    pub stride_bytes: u32,
21    /// Number of rows.
22    pub count: u32,
23}
24
25impl BufferLayout {
26    /// Describe `count` rows of `components` scalars spaced `stride_bytes` apart.
27    pub const fn new(scalar: ScalarType, components: u32, stride_bytes: u32, count: u32) -> Self {
28        Self {
29            scalar,
30            components,
31            stride_bytes,
32            count,
33        }
34    }
35
36    /// Total buffer size in bytes.
37    pub const fn byte_len(&self) -> u64 {
38        self.stride_bytes as u64 * self.count as u64
39    }
40}
41
42/// How to interleave atom positions for upload.
43#[repr(u8)]
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
45pub enum PositionLayout {
46    /// Tightly packed `[x, y, z]`, 12-byte stride. Suits vertex buffers
47    /// (`Float32x3`).
48    Packed3 = 1,
49    /// `[x, y, z, 1.0]`, 16-byte stride. Required for storage/uniform buffers,
50    /// where WGSL `array<vec3<f32>>` and std140/std430 `vec3` arrays use a
51    /// 16-byte stride; uploading `Packed3` there silently misreads every atom
52    /// after the first.
53    Padded4 = 2,
54}
55
56impl PositionLayout {
57    /// Components per position.
58    pub const fn components(self) -> u32 {
59        match self {
60            PositionLayout::Packed3 => 3,
61            PositionLayout::Padded4 => 4,
62        }
63    }
64
65    /// Bytes per position.
66    pub const fn stride_bytes(self) -> u32 {
67        self.components() * 4
68    }
69
70    /// Layout of a buffer holding `count` positions.
71    pub const fn buffer_layout(self, count: u32) -> BufferLayout {
72        BufferLayout::new(
73            ScalarType::F32,
74            self.components(),
75            self.stride_bytes(),
76            count,
77        )
78    }
79}
80
81/// Bytes ready to upload, with their layout.
82///
83/// `bytes` is little-endian, matching every GPU and wasm32. It borrows when
84/// the source column is already laid out correctly (see
85/// [`GpuBuffer::from_column`]) and owns an interleaved copy otherwise.
86#[derive(Clone, Debug, PartialEq)]
87pub struct GpuBuffer<'a> {
88    /// Debug label, suitable for a GPU buffer label.
89    pub label: &'static str,
90    /// Memory layout of `bytes`.
91    pub layout: BufferLayout,
92    /// Buffer contents; `bytes.len() == layout.byte_len()`.
93    pub bytes: Cow<'a, [u8]>,
94}
95
96impl<'a> GpuBuffer<'a> {
97    /// Wrap a scalar column as a one-component buffer; zero-copy on
98    /// little-endian targets.
99    ///
100    /// Columns obtained from [`TopologyColumns`](super::TopologyColumns) or
101    /// [`FrameColumns`](super::FrameColumns) are already bounded, so this only
102    /// fails for hand-built columns longer than `u32::MAX` rows.
103    pub fn from_column(column: Column<'a>) -> Result<Self, BoundaryError> {
104        let scalar = column.data.scalar_type();
105        Ok(GpuBuffer {
106            label: column.name,
107            layout: BufferLayout::new(
108                scalar,
109                1,
110                scalar.size_bytes() as u32,
111                to_u32("column row count", column.data.len())?,
112            ),
113            bytes: column.data.le_bytes(),
114        })
115    }
116}