362 lines
11 KiB
Rust
362 lines
11 KiB
Rust
// Zero-Copy UI State Transfer - Bevy UI 0.15+
|
|
// Production-ready zero-copy UI state transfer patterns
|
|
//
|
|
// Last Updated: 2026-03-14
|
|
// Bevy Version: 0.15+
|
|
// Rust Version: 1.85+
|
|
|
|
use bevy::prelude::*;
|
|
use bevy::render::render_resource::{Buffer, BufferInitDescriptor, BufferUsages};
|
|
use std::mem::{size_of, slice_from_raw_parts};
|
|
|
|
/// Zero-Copy UI State - Bytemuck derive for safe zero-copy transfer
|
|
/// This pattern eliminates serialization overhead for high-performance UI
|
|
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
|
|
#[repr(C)]
|
|
pub struct ZeroCopyUiState {
|
|
/// Player health - direct memory transfer
|
|
pub health: u32,
|
|
/// Player mana - direct memory transfer
|
|
pub mana: u32,
|
|
/// Player experience - direct memory transfer
|
|
pub experience: u32,
|
|
/// Player level - direct memory transfer
|
|
pub level: u32,
|
|
/// Position X - GPU coordinate transfer
|
|
pub position_x: f32,
|
|
/// Position Y - GPU coordinate transfer
|
|
pub position_y: f32,
|
|
/// Theme ID - direct enum transfer
|
|
pub theme_id: u8,
|
|
/// Accessibility flags - bitfield
|
|
pub accessibility_flags: u8,
|
|
/// Padding for alignment
|
|
_padding: [u8; 2],
|
|
}
|
|
|
|
/// Bytemuck trait implementation - Safe zero-copy guarantees
|
|
/// UNSAFE: Requires careful memory layout verification
|
|
unsafe impl Pod for ZeroCopyUiState {
|
|
/// All bytes are valid - no invalid representations
|
|
/// Verified by #[repr(C)] and primitive types only
|
|
}
|
|
|
|
/// Zeroable trait implementation - Zero initialization safe
|
|
/// UNSAFE: All fields can be zero-initialized
|
|
unsafe impl Zeroable for ZeroCopyUiState {
|
|
/// Zero value is valid for all fields
|
|
/// u32, f32, u8 all have valid zero representations
|
|
}
|
|
|
|
/// Pod trait - Plain Old Data for zero-copy
|
|
/// All types must be Copy + Clone
|
|
pub trait Pod: Copy + Clone {
|
|
/// Cast slice reference - zero-copy conversion
|
|
fn cast_slice(&self) -> &[u8] {
|
|
unsafe {
|
|
slice_from_raw_parts(
|
|
self as *const Self as *const u8,
|
|
size_of::<Self>(),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Cast mutable slice - zero-copy mutation
|
|
fn cast_slice_mut(&mut self) -> &[u8] {
|
|
unsafe {
|
|
slice_from_raw_parts(
|
|
self as *mut Self as *const u8,
|
|
size_of::<Self>(),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Size in bytes - constant
|
|
fn size() -> usize {
|
|
size_of::<Self>()
|
|
}
|
|
}
|
|
|
|
/// Zeroable trait - Zero initialization
|
|
pub trait Zeroable: Pod {
|
|
/// Zero value - all bits zero
|
|
fn zero() -> Self {
|
|
unsafe {
|
|
let mut value = Self::default();
|
|
std::ptr::write_bytes(&mut value as *mut Self, 0, 1);
|
|
value
|
|
}
|
|
}
|
|
|
|
/// Is zero-initialized valid?
|
|
fn is_zero_valid() -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
/// GPU Buffer Transfer - Zero-copy upload to GPU
|
|
#[derive(Resource, Debug)]
|
|
pub struct GpuUiBuffer {
|
|
/// GPU buffer
|
|
pub buffer: Buffer,
|
|
/// Buffer size
|
|
pub size: u64,
|
|
/// Buffer usage flags
|
|
pub usages: BufferUsages,
|
|
/// Last upload sequence
|
|
pub sequence: u64,
|
|
}
|
|
|
|
/// Implementation: ZeroCopyUiState
|
|
impl ZeroCopyUiState {
|
|
/// Create from ECS state - zero-copy conversion
|
|
pub fn from_ecs(state: &UiStateComponent) -> Self {
|
|
Self {
|
|
health: state.health,
|
|
mana: state.mana,
|
|
experience: state.experience,
|
|
level: state.level,
|
|
position_x: state.position_x,
|
|
position_y: state.position_y,
|
|
theme_id: match state.theme {
|
|
UiTheme::Default => 0,
|
|
UiTheme::HighContrast => 1,
|
|
UiTheme::Dark => 2,
|
|
UiTheme::Light => 3,
|
|
UiTheme::ColorblindProtanopia => 4,
|
|
UiTheme::ColorblindDeuteranopia => 5,
|
|
UiTheme::ColorblindTritanopia => 6,
|
|
},
|
|
accessibility_flags: {
|
|
let mut flags = 0u8;
|
|
if state.accessibility.screen_reader { flags |= 0b00000001; }
|
|
if state.accessibility.high_contrast { flags |= 0b00000010; }
|
|
if state.accessibility.reduced_motion { flags |= 0b00000100; }
|
|
if state.accessibility.keyboard_navigation { flags |= 0b00001000; }
|
|
if state.accessibility.focus_indicators { flags |= 0b00010000; }
|
|
flags
|
|
},
|
|
_padding: [0; 2],
|
|
}
|
|
}
|
|
|
|
/// Copy from ECS state - direct memory transfer
|
|
pub fn copy_from(&mut self, state: &UiStateComponent) {
|
|
*self = Self::from_ecs(state);
|
|
}
|
|
|
|
/// Copy to ECS state - direct memory transfer
|
|
pub fn copy_to(&self, state: &mut UiStateComponent) {
|
|
state.health = self.health;
|
|
state.mana = self.mana;
|
|
state.experience = self.experience;
|
|
state.level = self.level;
|
|
state.position_x = self.position_x;
|
|
state.position_y = self.position_y;
|
|
state.theme = match self.theme_id {
|
|
0 => UiTheme::Default,
|
|
1 => UiTheme::HighContrast,
|
|
2 => UiTheme::Dark,
|
|
3 => UiTheme::Light,
|
|
4 => UiTheme::ColorblindProtanopia,
|
|
5 => UiTheme::ColorblindDeuteranopia,
|
|
6 => UiTheme::ColorblindTritanopia,
|
|
_ => UiTheme::Default,
|
|
};
|
|
state.accessibility.screen_reader = (self.accessibility_flags & 0b00000001) != 0;
|
|
state.accessibility.high_contrast = (self.accessibility_flags & 0b00000010) != 0;
|
|
state.accessibility.reduced_motion = (self.accessibility_flags & 0b00000100) != 0;
|
|
state.accessibility.keyboard_navigation = (self.accessibility_flags & 0b00001000) != 0;
|
|
state.accessibility.focus_indicators = (self.accessibility_flags & 0b00010000) != 0;
|
|
}
|
|
|
|
/// Validate memory layout - zero-copy safety
|
|
pub fn validate_layout() -> bool {
|
|
// Verify alignment
|
|
assert!(size_of::<Self>() % 4 == 0, "Size must be 4-byte aligned");
|
|
// Verify field offsets
|
|
assert!(size_of::<u32>() == 4, "u32 size expected");
|
|
assert!(size_of::<f32>() == 4, "f32 size expected");
|
|
true
|
|
}
|
|
|
|
/// Get byte slice - zero-copy read
|
|
pub fn as_bytes(&self) -> &[u8] {
|
|
self.cast_slice()
|
|
}
|
|
|
|
/// Get mutable byte slice - zero-copy write
|
|
pub fn as_bytes_mut(&mut self) -> &[u8] {
|
|
self.cast_slice_mut()
|
|
}
|
|
}
|
|
|
|
/// Implementation: Pod for ZeroCopyUiState
|
|
impl Pod for ZeroCopyUiState {
|
|
fn size() -> usize {
|
|
size_of::<Self>()
|
|
}
|
|
}
|
|
|
|
/// Implementation: Zeroable for ZeroCopyUiState
|
|
impl Zeroable for ZeroCopyUiState {
|
|
fn zero() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
fn is_zero_valid() -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
/// ECS UI State - Source for zero-copy transfer
|
|
#[derive(Component, Resource, Clone, Debug, Default)]
|
|
pub struct UiStateComponent {
|
|
pub health: u32,
|
|
pub mana: u32,
|
|
pub experience: u32,
|
|
pub level: u32,
|
|
pub position_x: f32,
|
|
pub position_y: f32,
|
|
pub theme: UiTheme,
|
|
pub accessibility: AccessibilityConfig,
|
|
}
|
|
|
|
/// Accessibility Config - Flags for zero-copy
|
|
#[derive(Component, Clone, Debug, Default)]
|
|
pub struct AccessibilityConfig {
|
|
pub screen_reader: bool,
|
|
pub high_contrast: bool,
|
|
pub reduced_motion: bool,
|
|
pub keyboard_navigation: bool,
|
|
pub focus_indicators: bool,
|
|
}
|
|
|
|
/// UI Theme - Enum for zero-copy ID
|
|
#[derive(Component, Clone, Debug, Default, PartialEq, Eq)]
|
|
pub enum UiTheme {
|
|
#[default]
|
|
Default,
|
|
HighContrast,
|
|
Dark,
|
|
Light,
|
|
ColorblindProtanopia,
|
|
ColorblindDeuteranopia,
|
|
ColorblindTritanopia,
|
|
}
|
|
|
|
/// System: Zero-copy transfer system
|
|
pub fn zero_copy_transfer_system(
|
|
ecs_state: Res<UiStateComponent>,
|
|
mut zero_copy: ResMut<ZeroCopyUiState>,
|
|
) {
|
|
// Direct memory transfer - no allocation
|
|
zero_copy.copy_from(&ecs_state);
|
|
}
|
|
|
|
/// System: GPU buffer upload system
|
|
pub fn gpu_buffer_upload_system(
|
|
zero_copy: Res<ZeroCopyUiState>,
|
|
mut gpu_buffer: ResMut<GpuUiBuffer>,
|
|
render_device: Res<RenderDevice>,
|
|
) {
|
|
// Zero-copy GPU upload - direct memory write
|
|
let bytes = zero_copy.as_bytes();
|
|
|
|
// Write buffer directly - no intermediate allocation
|
|
render_device.queue_buffer_write(
|
|
&mut gpu_buffer.buffer,
|
|
0,
|
|
bytes,
|
|
);
|
|
|
|
gpu_buffer.sequence += 1;
|
|
log::info!("GPU buffer uploaded: sequence {}", gpu_buffer.sequence);
|
|
}
|
|
|
|
/// System: Batch transfer system - Zero-copy slice transfer
|
|
pub fn batch_transfer_system(
|
|
states: Query<&UiStateComponent>,
|
|
mut zero_copy_states: QueryMut<ZeroCopyUiState>,
|
|
) {
|
|
// Batch zero-copy transfer - no per-entity allocation
|
|
for (ecs, zero_copy) in states.iter().zip(zero_copy_states.iter_mut()) {
|
|
zero_copy.copy_from(ecs);
|
|
}
|
|
}
|
|
|
|
/// Create GPU buffer - Zero-copy initialization
|
|
pub fn create_gpu_buffer(render_device: &RenderDevice, size: u64) -> GpuUiBuffer {
|
|
let buffer = render_device.create_buffer(&BufferInitDescriptor {
|
|
label: "UI State Buffer",
|
|
size,
|
|
usage: BufferUsages::MAP_WRITE | BufferUsages::COPY_DST,
|
|
});
|
|
|
|
GpuUiBuffer {
|
|
buffer,
|
|
size,
|
|
usages: BufferUsages::MAP_WRITE | BufferUsages::COPY_DST,
|
|
sequence: 0,
|
|
}
|
|
}
|
|
|
|
/// Validate zero-copy guarantees - Runtime checks
|
|
pub fn validate_zero_copy_guarantees() {
|
|
let state = ZeroCopyUiState::default();
|
|
|
|
// Size validation
|
|
assert!(state.size() % 4 == 0, "Size must be aligned");
|
|
|
|
// Byte slice validation
|
|
let bytes = state.as_bytes();
|
|
assert_eq!(bytes.len(), size_of::<ZeroCopyUiState>(), "Byte slice size mismatch");
|
|
|
|
// Zero initialization validation
|
|
let zero = ZeroCopyUiState::zero();
|
|
assert!(ZeroCopyUiState::is_zero_valid(), "Zero must be valid");
|
|
|
|
log::info!("Zero-copy guarantees validated");
|
|
}
|
|
|
|
/// Performance benchmark - Zero-copy vs serialization
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_zero_copy_size() {
|
|
let state = ZeroCopyUiState::default();
|
|
assert_eq!(state.size(), 24); // 6 u32/f32 + 2 u8 + 2 padding
|
|
}
|
|
|
|
#[test]
|
|
fn test_zero_copy_bytes() {
|
|
let state = ZeroCopyUiState::default();
|
|
let bytes = state.as_bytes();
|
|
assert_eq!(bytes.len(), size_of::<ZeroCopyUiState>());
|
|
}
|
|
|
|
#[test]
|
|
fn test_zero_copy_from_ecs() {
|
|
let ecs = UiStateComponent {
|
|
health: 100,
|
|
mana: 50,
|
|
experience: 500,
|
|
level: 10,
|
|
position_x: 1.0,
|
|
position_y: 2.0,
|
|
theme: UiTheme::HighContrast,
|
|
accessibility: AccessibilityConfig::default(),
|
|
};
|
|
|
|
let zero_copy = ZeroCopyUiState::from_ecs(&ecs);
|
|
assert_eq!(zero_copy.health, 100);
|
|
assert_eq!(zero_copy.theme_id, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_zero_copy_layout() {
|
|
assert!(ZeroCopyUiState::validate_layout());
|
|
}
|
|
} |