core: writer-owned relocation
The worker no longer copies rows when an array cannot grow in place. It reserves a zeroed region, returns the staged descriptor with a commit flag, and queues every other request until the main thread (the only writer) has copied the old bytes and sent commit-rows. Writes that raced the old worker-side copy were lost; lights in the glTF viewer regularly ended up at the origin because of it. Amp-Thread-ID: https://ampcode.com/threads/T-01a0b2dd-db32-712d-99ce-12ce4a86d979 Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
+79
-28
@@ -27,15 +27,19 @@ impl FreeList {
|
||||
|
||||
/// 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 {
|
||||
let Some((&block, &block_size)) = self.0.range(..=offset).next_back() else {
|
||||
return false;
|
||||
};
|
||||
if available < size {
|
||||
if offset + size > block + block_size {
|
||||
return false;
|
||||
}
|
||||
self.0.remove(&offset);
|
||||
if available > size {
|
||||
self.0.insert(offset + size, available - size);
|
||||
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);
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -58,7 +62,7 @@ impl FreeList {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
pub struct Rows {
|
||||
pub name: String,
|
||||
pub rows: u32,
|
||||
@@ -82,6 +86,8 @@ pub struct RenderData {
|
||||
free: FreeList,
|
||||
rows: HashMap<String, Rows>,
|
||||
slots: HashMap<String, Slots>,
|
||||
/// Relocated descriptors the writer has not committed yet; `rows` keeps the old storage live.
|
||||
pending: HashMap<String, Rows>,
|
||||
}
|
||||
|
||||
impl RenderData {
|
||||
@@ -99,19 +105,22 @@ impl RenderData {
|
||||
free: FreeList(BTreeMap::from([(0, capacity)])),
|
||||
rows: HashMap::new(),
|
||||
slots: HashMap::new(),
|
||||
pending: HashMap::new(),
|
||||
};
|
||||
data.create_rows("signals".into(), 1, 32, "f32".into())?;
|
||||
data.write_signal(5, 1.0);
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// Creates or grows an array. The flag is set when the rows moved: the returned descriptor
|
||||
/// is then staged and the writer must copy the old bytes into it and `commit` it.
|
||||
pub fn create_rows(
|
||||
&mut self,
|
||||
name: String,
|
||||
rows: u32,
|
||||
stride: u32,
|
||||
format: String,
|
||||
) -> Result<Rows, &'static str> {
|
||||
) -> Result<(Rows, bool), &'static str> {
|
||||
if name.is_empty()
|
||||
|| rows == 0
|
||||
|| stride < 16
|
||||
@@ -125,19 +134,19 @@ impl RenderData {
|
||||
return Err("ROWS");
|
||||
}
|
||||
if rows <= current.rows {
|
||||
return Ok(current);
|
||||
return Ok((current, false));
|
||||
}
|
||||
if name == "signals" {
|
||||
return Err("ROWS_BUILTIN");
|
||||
}
|
||||
let added = rows - current.rows;
|
||||
let descriptor = self.grow_rows(current, rows)?;
|
||||
let (descriptor, moved) = self.grow_rows(current, rows)?;
|
||||
self.slots
|
||||
.get_mut(&descriptor.name)
|
||||
.unwrap()
|
||||
.free
|
||||
.release(descriptor.rows - added, added);
|
||||
return Ok(descriptor);
|
||||
return Ok((descriptor, moved));
|
||||
}
|
||||
let bytes = rows.checked_mul(stride).ok_or("ARENA_OOM")?;
|
||||
let offset = self.reserve(bytes)?;
|
||||
@@ -155,13 +164,16 @@ impl RenderData {
|
||||
let mut slots = Slots::default();
|
||||
slots.free.release(0, rows);
|
||||
self.slots.insert(descriptor.name.clone(), slots);
|
||||
Ok(descriptor)
|
||||
Ok((descriptor, false))
|
||||
}
|
||||
|
||||
pub fn delete_rows(&mut self, name: &str) -> Result<(), &'static str> {
|
||||
if name == "signals" {
|
||||
return Err("ROWS_BUILTIN");
|
||||
}
|
||||
if self.pending.contains_key(name) {
|
||||
return Err("ROWS_PENDING");
|
||||
}
|
||||
if self
|
||||
.slots
|
||||
.get(name)
|
||||
@@ -192,7 +204,12 @@ impl RenderData {
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
/// The growth flags mirror `create_rows`: `(grew, moved)`.
|
||||
pub fn allocate_object(
|
||||
&mut self,
|
||||
name: &str,
|
||||
count: u32,
|
||||
) -> Result<(u32, bool, bool), &'static str> {
|
||||
if name == "signals" {
|
||||
return Err("ROWS_BUILTIN");
|
||||
}
|
||||
@@ -201,6 +218,7 @@ impl RenderData {
|
||||
}
|
||||
let slots = self.slots.get_mut(name).ok_or("ROWS_UNKNOWN")?;
|
||||
let mut grew = false;
|
||||
let mut moved = false;
|
||||
let id = match slots.free.allocate(count, 1) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
@@ -211,7 +229,7 @@ impl RenderData {
|
||||
.ok_or("OBJECT_LIMIT")?
|
||||
.max(descriptor.rows.checked_add(count).ok_or("OBJECT_LIMIT")?);
|
||||
let old_rows = descriptor.rows;
|
||||
self.grow_rows(descriptor, rows)?;
|
||||
moved = self.grow_rows(descriptor, rows)?.1;
|
||||
grew = true;
|
||||
let slots = self.slots.get_mut(name).unwrap();
|
||||
slots.free.release(old_rows, rows - old_rows);
|
||||
@@ -219,7 +237,35 @@ impl RenderData {
|
||||
}
|
||||
};
|
||||
self.slots.get_mut(name).unwrap().spans.insert(id, count);
|
||||
Ok((id, grew))
|
||||
Ok((id, grew, moved))
|
||||
}
|
||||
|
||||
/// The descriptor the writer should use: staged rows win over committed ones.
|
||||
pub fn staged(&self, name: &str) -> Option<&Rows> {
|
||||
self.pending.get(name).or_else(|| self.rows.get(name))
|
||||
}
|
||||
|
||||
/// Publishes staged relocations after the writer copied the old bytes into them.
|
||||
pub fn commit(&mut self, names: &[String]) -> Result<(), &'static str> {
|
||||
for name in names {
|
||||
let staged = self.pending.remove(name).ok_or("ROWS_PENDING")?;
|
||||
let old = self.rows.insert(name.clone(), staged).unwrap();
|
||||
self.release(old.offset - self.base, old.bytes);
|
||||
}
|
||||
if !self.pending.is_empty() {
|
||||
return Err("ROWS_PENDING");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Drops every staged relocation, for example when a batch fails half-way.
|
||||
pub fn discard_pending(&mut self) {
|
||||
for staged in std::mem::take(&mut self.pending).into_values() {
|
||||
self.release(staged.offset - self.base, staged.bytes);
|
||||
let rows = self.rows[&staged.name].rows;
|
||||
let slots = self.slots.get_mut(&staged.name).unwrap();
|
||||
assert!(slots.free.take(rows, staged.rows - rows));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_object(&mut self, name: &str, id: u32) -> Result<(), &'static str> {
|
||||
@@ -232,31 +278,36 @@ impl RenderData {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn grow_rows(&mut self, mut descriptor: Rows, rows: u32) -> Result<Rows, &'static str> {
|
||||
/// Extends in place when the next arena block is free; otherwise stages a zeroed region for
|
||||
/// the writer to fill and `commit`, because only the writer knows when its last write landed.
|
||||
fn grow_rows(&mut self, mut descriptor: Rows, rows: u32) -> Result<(Rows, bool), &'static str> {
|
||||
if self.pending.contains_key(&descriptor.name) {
|
||||
return Err("ROWS_PENDING");
|
||||
}
|
||||
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 moved = !self.free.take(old_end, extra);
|
||||
if moved {
|
||||
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;
|
||||
} else {
|
||||
self.arena[self.arena_start + old_end as usize
|
||||
..self.arena_start + (old_end + extra) as usize]
|
||||
.fill(0);
|
||||
}
|
||||
descriptor.rows = rows;
|
||||
descriptor.bytes = bytes;
|
||||
self.rows
|
||||
.insert(descriptor.name.clone(), descriptor.clone());
|
||||
Ok(descriptor)
|
||||
if moved {
|
||||
&mut self.pending
|
||||
} else {
|
||||
&mut self.rows
|
||||
}
|
||||
.insert(descriptor.name.clone(), descriptor.clone());
|
||||
Ok((descriptor, moved))
|
||||
}
|
||||
|
||||
pub fn update_signals(&mut self, delta: f32, frame: u32, elapsed: f32, fps: u32) {
|
||||
|
||||
Reference in New Issue
Block a user