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}