Amp-Thread-ID: https://ampcode.com/threads/T-01a0b2dd-db32-712d-99ce-12ce4a86d979 Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
318 lines
10 KiB
Rust
318 lines
10 KiB
Rust
use std::collections::{BTreeMap, HashMap};
|
|
|
|
use serde::Serialize;
|
|
|
|
const ALIGNMENT: u32 = 64;
|
|
|
|
/// First-fit interval allocator over `u32` offsets; adjacent free ranges coalesce on release.
|
|
#[derive(Default)]
|
|
struct FreeList(BTreeMap<u32, u32>);
|
|
|
|
impl FreeList {
|
|
fn allocate(&mut self, size: u32, align: u32) -> Option<u32> {
|
|
let (block, block_size, offset) = self.0.iter().find_map(|(&block, &block_size)| {
|
|
let offset = block.checked_add(align - 1)? & !(align - 1);
|
|
(offset.checked_add(size)? <= block + block_size).then_some((block, block_size, offset))
|
|
})?;
|
|
self.0.remove(&block);
|
|
if offset > block {
|
|
self.0.insert(block, offset - block);
|
|
}
|
|
let end = offset + size;
|
|
if end < block + block_size {
|
|
self.0.insert(end, block + block_size - end);
|
|
}
|
|
Some(offset)
|
|
}
|
|
|
|
/// Claims exactly `[offset, offset + size)` when that range is entirely free.
|
|
fn take(&mut self, offset: u32, size: u32) -> bool {
|
|
let Some(&available) = self.0.get(&offset) else {
|
|
return false;
|
|
};
|
|
if available < size {
|
|
return false;
|
|
}
|
|
self.0.remove(&offset);
|
|
if available > size {
|
|
self.0.insert(offset + size, available - size);
|
|
}
|
|
true
|
|
}
|
|
|
|
fn release(&mut self, mut offset: u32, mut size: u32) {
|
|
if let Some((&previous, &previous_size)) = self.0.range(..offset).next_back() {
|
|
if previous + previous_size == offset {
|
|
self.0.remove(&previous);
|
|
offset = previous;
|
|
size += previous_size;
|
|
}
|
|
}
|
|
if let Some((&next, &next_size)) = self.0.range(offset..).next() {
|
|
if offset + size == next {
|
|
self.0.remove(&next);
|
|
size += next_size;
|
|
}
|
|
}
|
|
self.0.insert(offset, size);
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Serialize)]
|
|
pub struct Rows {
|
|
pub name: String,
|
|
pub rows: u32,
|
|
pub stride: u32,
|
|
pub format: String,
|
|
pub offset: u32,
|
|
pub bytes: u32,
|
|
}
|
|
|
|
/// Row spans handed out by `allocate_object`, keyed by their first row.
|
|
#[derive(Default)]
|
|
struct Slots {
|
|
free: FreeList,
|
|
spans: HashMap<u32, u32>,
|
|
}
|
|
|
|
pub struct RenderData {
|
|
arena: Box<[u8]>,
|
|
arena_start: usize,
|
|
base: u32,
|
|
free: FreeList,
|
|
rows: HashMap<String, Rows>,
|
|
slots: HashMap<String, Slots>,
|
|
}
|
|
|
|
impl RenderData {
|
|
pub fn new(capacity: u32) -> Result<Self, &'static str> {
|
|
if capacity < ALIGNMENT || capacity > u32::MAX - (ALIGNMENT - 1) {
|
|
return Err("INIT");
|
|
}
|
|
let mut arena = vec![0; (capacity + ALIGNMENT - 1) as usize].into_boxed_slice();
|
|
let pointer = arena.as_mut_ptr() as usize;
|
|
let aligned = (pointer + ALIGNMENT as usize - 1) & !(ALIGNMENT as usize - 1);
|
|
let mut data = Self {
|
|
arena,
|
|
arena_start: aligned - pointer,
|
|
base: aligned as u32,
|
|
free: FreeList(BTreeMap::from([(0, capacity)])),
|
|
rows: HashMap::new(),
|
|
slots: HashMap::new(),
|
|
};
|
|
data.create_rows("signals".into(), 1, 32, "f32".into())?;
|
|
data.write_signal(5, 1.0);
|
|
Ok(data)
|
|
}
|
|
|
|
pub fn create_rows(
|
|
&mut self,
|
|
name: String,
|
|
rows: u32,
|
|
stride: u32,
|
|
format: String,
|
|
) -> Result<Rows, &'static str> {
|
|
if name.is_empty()
|
|
|| rows == 0
|
|
|| stride < 16
|
|
|| stride % 16 != 0
|
|
|| !matches!(format.as_str(), "f32" | "u32" | "i32")
|
|
{
|
|
return Err("ROWS");
|
|
}
|
|
if let Some(current) = self.rows.get(&name).cloned() {
|
|
if current.stride != stride || current.format != format {
|
|
return Err("ROWS");
|
|
}
|
|
if rows <= current.rows {
|
|
return Ok(current);
|
|
}
|
|
if name == "signals" {
|
|
return Err("ROWS_BUILTIN");
|
|
}
|
|
let added = rows - current.rows;
|
|
let descriptor = self.grow_rows(current, rows)?;
|
|
self.slots
|
|
.get_mut(&descriptor.name)
|
|
.unwrap()
|
|
.free
|
|
.release(descriptor.rows - added, added);
|
|
return Ok(descriptor);
|
|
}
|
|
let bytes = rows.checked_mul(stride).ok_or("ARENA_OOM")?;
|
|
let offset = self.reserve(bytes)?;
|
|
let end = offset + bytes;
|
|
self.arena[self.arena_start + offset as usize..self.arena_start + end as usize].fill(0);
|
|
let descriptor = Rows {
|
|
name: name.clone(),
|
|
rows,
|
|
stride,
|
|
format,
|
|
offset: self.base + offset,
|
|
bytes,
|
|
};
|
|
self.rows.insert(name, descriptor.clone());
|
|
let mut slots = Slots::default();
|
|
slots.free.release(0, rows);
|
|
self.slots.insert(descriptor.name.clone(), slots);
|
|
Ok(descriptor)
|
|
}
|
|
|
|
pub fn delete_rows(&mut self, name: &str) -> Result<(), &'static str> {
|
|
if name == "signals" {
|
|
return Err("ROWS_BUILTIN");
|
|
}
|
|
if self
|
|
.slots
|
|
.get(name)
|
|
.is_some_and(|slots| !slots.spans.is_empty())
|
|
{
|
|
return Err("ROWS_ACTIVE");
|
|
}
|
|
let rows = self.rows.remove(name).ok_or("ROWS_UNKNOWN")?;
|
|
self.slots.remove(name);
|
|
self.release(rows.offset - self.base, rows.bytes);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn rows(&self, name: &str) -> Option<&Rows> {
|
|
self.rows.get(name)
|
|
}
|
|
|
|
pub fn descriptors(&self) -> Vec<Rows> {
|
|
let mut rows = self.rows.values().cloned().collect::<Vec<_>>();
|
|
rows.sort_by_key(|rows| rows.offset);
|
|
rows
|
|
}
|
|
|
|
pub fn bytes(&self, name: &str) -> Option<&[u8]> {
|
|
let rows = self.rows(name)?;
|
|
let start = self.arena_start + (rows.offset - self.base) as usize;
|
|
Some(&self.arena[start..start + rows.bytes as usize])
|
|
}
|
|
|
|
/// Reserves `count` contiguous rows and returns the first one; grows geometrically when full.
|
|
pub fn allocate_object(&mut self, name: &str, count: u32) -> Result<(u32, bool), &'static str> {
|
|
if name == "signals" {
|
|
return Err("ROWS_BUILTIN");
|
|
}
|
|
if count == 0 {
|
|
return Err("ROWS");
|
|
}
|
|
let slots = self.slots.get_mut(name).ok_or("ROWS_UNKNOWN")?;
|
|
let mut grew = false;
|
|
let id = match slots.free.allocate(count, 1) {
|
|
Some(id) => id,
|
|
None => {
|
|
let descriptor = self.rows[name].clone();
|
|
let rows = descriptor
|
|
.rows
|
|
.checked_mul(2)
|
|
.ok_or("OBJECT_LIMIT")?
|
|
.max(descriptor.rows.checked_add(count).ok_or("OBJECT_LIMIT")?);
|
|
let old_rows = descriptor.rows;
|
|
self.grow_rows(descriptor, rows)?;
|
|
grew = true;
|
|
let slots = self.slots.get_mut(name).unwrap();
|
|
slots.free.release(old_rows, rows - old_rows);
|
|
slots.free.allocate(count, 1).unwrap()
|
|
}
|
|
};
|
|
self.slots.get_mut(name).unwrap().spans.insert(id, count);
|
|
Ok((id, grew))
|
|
}
|
|
|
|
pub fn delete_object(&mut self, name: &str, id: u32) -> Result<(), &'static str> {
|
|
let slots = self.slots.get_mut(name).ok_or("ROWS_UNKNOWN")?;
|
|
let count = slots.spans.remove(&id).ok_or("OBJECT_UNKNOWN")?;
|
|
slots.free.release(id, count);
|
|
let rows = self.rows[name].clone();
|
|
let start = self.arena_start + (rows.offset - self.base + id * rows.stride) as usize;
|
|
self.arena[start..start + (count * rows.stride) as usize].fill(0);
|
|
Ok(())
|
|
}
|
|
|
|
fn grow_rows(&mut self, mut descriptor: Rows, rows: u32) -> Result<Rows, &'static str> {
|
|
let bytes = rows.checked_mul(descriptor.stride).ok_or("ARENA_OOM")?;
|
|
let old_offset = descriptor.offset - self.base;
|
|
let old_end = old_offset + descriptor.bytes;
|
|
let extra = bytes - descriptor.bytes;
|
|
if self.free.take(old_end, extra) {
|
|
self.arena[self.arena_start + old_end as usize
|
|
..self.arena_start + (old_end + extra) as usize]
|
|
.fill(0);
|
|
} else {
|
|
let offset = self.reserve(bytes)?;
|
|
let start = self.arena_start + offset as usize;
|
|
self.arena[start..start + bytes as usize].fill(0);
|
|
self.arena.copy_within(
|
|
self.arena_start + old_offset as usize..self.arena_start + old_end as usize,
|
|
start,
|
|
);
|
|
self.release(old_offset, descriptor.bytes);
|
|
descriptor.offset = self.base + offset;
|
|
}
|
|
descriptor.rows = rows;
|
|
descriptor.bytes = bytes;
|
|
self.rows
|
|
.insert(descriptor.name.clone(), descriptor.clone());
|
|
Ok(descriptor)
|
|
}
|
|
|
|
pub fn update_signals(&mut self, delta: f32, frame: u32, elapsed: f32, fps: u32) {
|
|
let start = self.arena_start + (self.rows["signals"].offset - self.base) as usize;
|
|
for (index, value) in [delta, frame as f32, elapsed, fps as f32]
|
|
.iter()
|
|
.enumerate()
|
|
{
|
|
self.arena[start + index * 4..start + index * 4 + 4]
|
|
.copy_from_slice(&value.to_le_bytes());
|
|
}
|
|
}
|
|
|
|
pub fn render_pending(&self) -> bool {
|
|
self.signal(4) == 0.0 && self.signal(5) != 0.0 && self.signal(6) == 0.0
|
|
}
|
|
|
|
pub fn begin_render(&mut self) -> bool {
|
|
if !self.render_pending() {
|
|
return false;
|
|
}
|
|
self.write_signal(5, 0.0);
|
|
true
|
|
}
|
|
|
|
pub fn mark_dirty(&mut self) {
|
|
self.write_signal(5, 1.0);
|
|
}
|
|
|
|
pub fn loadout_ready(&mut self) {
|
|
self.write_signal(6, 0.0);
|
|
self.mark_dirty();
|
|
}
|
|
|
|
fn signal(&self, index: usize) -> f32 {
|
|
let rows = &self.rows["signals"];
|
|
let start = self.arena_start + (rows.offset - self.base) as usize + index * 4;
|
|
f32::from_le_bytes(self.arena[start..start + 4].try_into().unwrap())
|
|
}
|
|
|
|
fn write_signal(&mut self, index: usize, value: f32) {
|
|
let rows = &self.rows["signals"];
|
|
let start = self.arena_start + (rows.offset - self.base) as usize + index * 4;
|
|
self.arena[start..start + 4].copy_from_slice(&value.to_le_bytes());
|
|
}
|
|
|
|
fn reserve(&mut self, bytes: u32) -> Result<u32, &'static str> {
|
|
self.free.allocate(bytes, ALIGNMENT).ok_or("ARENA_OOM")
|
|
}
|
|
|
|
fn release(&mut self, offset: u32, bytes: u32) {
|
|
self.free.release(offset, bytes);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "render_data_tests.rs"]
|
|
mod tests;
|