From bcc1998c540ea246c0a782be2998b7ebad523921 Mon Sep 17 00:00:00 2001 From: Orual Date: Mon, 3 Aug 2026 08:13:52 -0400 Subject: [PATCH] PM-80: harden compound resource and worker contracts Epic: PM-86 Task: PM-80 PM-80: resolve renderer context and admission conflict Epic: PM-86 Task: PM-80 --- .../polymodel-renderer-worker/src/worker.rs | 438 ++---------------- 1 file changed, 49 insertions(+), 389 deletions(-) diff --git a/crates/polymodel-renderer-worker/src/worker.rs b/crates/polymodel-renderer-worker/src/worker.rs index 4da0570..aae94da 100644 --- a/crates/polymodel-renderer-worker/src/worker.rs +++ b/crates/polymodel-renderer-worker/src/worker.rs @@ -5,7 +5,6 @@ //! and the postcard message protocol bridge to the Dioxus main thread. use std::cell::RefCell; -use std::collections::HashMap; use std::rc::Rc; use std::sync::Arc; @@ -158,20 +157,20 @@ impl WorkerState { return; } }; - let canvas_reservation = - match self - .ledger - .reserve(owner, ResourceClass::CanvasBackbuffers, canvas_bytes) - { - Ok(reservation) => reservation, - Err(error) => { - self.post_event(&RendererEvent::Error { - fatal: true, - message: format!("canvas admission: {error}"), - }); - return; - } - }; + let canvas_reservation = match self.ledger.reserve( + owner, + ResourceClass::CanvasBackbuffers, + canvas_bytes, + ) { + Ok(reservation) => reservation, + Err(error) => { + self.post_event(&RendererEvent::Error { + fatal: true, + message: format!("canvas admission: {error}"), + }); + return; + } + }; let opts = js_sys::Object::new(); let _ = js_sys::Reflect::set(&opts, &"desynchronized".into(), &JsValue::from_bool(true)); @@ -329,33 +328,15 @@ fn scene_cpu_bytes(model: &ModelMesh) -> Result { match &primitive.geometry { Geometry::Triangles(mesh) => { add_bytes(&mut bytes, (mesh.positions.len() as u64).saturating_mul(12))?; - add_bytes( - &mut bytes, - mesh.normals.as_ref().map_or(0, |v| (v.len() as u64) * 12), - )?; - add_bytes( - &mut bytes, - mesh.tangents.as_ref().map_or(0, |v| (v.len() as u64) * 16), - )?; - add_bytes( - &mut bytes, - mesh.uvs.as_ref().map_or(0, |v| (v.len() as u64) * 8), - )?; - add_bytes( - &mut bytes, - mesh.colors.as_ref().map_or(0, |v| (v.len() as u64) * 16), - )?; + add_bytes(&mut bytes, mesh.normals.as_ref().map_or(0, |v| (v.len() as u64) * 12))?; + add_bytes(&mut bytes, mesh.tangents.as_ref().map_or(0, |v| (v.len() as u64) * 16))?; + add_bytes(&mut bytes, mesh.uvs.as_ref().map_or(0, |v| (v.len() as u64) * 8))?; + add_bytes(&mut bytes, mesh.colors.as_ref().map_or(0, |v| (v.len() as u64) * 16))?; add_bytes(&mut bytes, mesh.indices.len().unwrap_or(0) as u64 * 4)?; } Geometry::Points(points) => { - add_bytes( - &mut bytes, - (points.positions.len() as u64).saturating_mul(12), - )?; - add_bytes( - &mut bytes, - points.colors.as_ref().map_or(0, |v| (v.len() as u64) * 16), - )?; + add_bytes(&mut bytes, (points.positions.len() as u64).saturating_mul(12))?; + add_bytes(&mut bytes, points.colors.as_ref().map_or(0, |v| (v.len() as u64) * 16))?; } } } @@ -410,8 +391,8 @@ fn admit_scene( let mut gpu_texture_bytes = 0; let mut textures = Vec::with_capacity(texture_inputs.len()); for input in texture_inputs { - let decoded = - texture_base_bytes(input).map_err(|error| format!("texture base charge: {error}"))?; + let decoded = texture_base_bytes(input) + .map_err(|error| format!("texture base charge: {error}"))?; let gpu = texture_charge(input) .map_err(|error| format!("texture charge: {error}"))? .checked_add(GPU_TEXTURE_OVERHEAD_BYTES) @@ -571,9 +552,7 @@ mod tests { .outcome, polymodel_renderer_protocol::TransitionOutcome::Committed ); - assert!( - matches!(reducer.state(), polymodel_renderer_protocol::LoadState::Loading(id) if *id == identity) - ); + assert!(matches!(reducer.state(), polymodel_renderer_protocol::LoadState::Loading(id) if *id == identity)); assert!(matches!( reducer .reduce(LoadEvent::LoadSucceeded { @@ -584,10 +563,7 @@ mod tests { .state, polymodel_renderer_protocol::LoadState::Installing(id) if id == identity )); - assert!(!matches!( - reducer.state(), - polymodel_renderer_protocol::LoadState::Ready(_) - )); + assert!(!matches!(reducer.state(), polymodel_renderer_protocol::LoadState::Ready(_))); assert!(matches!( reducer .reduce(LoadEvent::InstallSucceeded { @@ -659,10 +635,7 @@ mod tests { }) .unwrap_or_else(|error| panic!("{format:?} fixture should parse: {error:?}")); assert_eq!(mesh.format, format); - assert!( - mesh.triangle_count() > 0, - "{format:?} must contain triangles" - ); + assert!(mesh.triangle_count() > 0, "{format:?} must contain triangles"); } } @@ -1006,8 +979,6 @@ fn handle_load_mesh( identity, }); let (pw, ph) = (st.pixel_width, st.pixel_height); - st.scene_generation = st.scene_generation.saturating_add(1); - let scene_owner = owner(st.load_epoch, st.scene_generation); let Some(renderer) = st.renderer.as_ref() else { let error = LoadError { code: LoadErrorCode::Renderer, @@ -1027,28 +998,30 @@ fn handle_load_mesh( }); return; }; - let (scene_reservations, texture_reservations) = - match admit_scene(&st.ledger, scene_owner, &model) { - Ok(reservations) => reservations, - Err(message) => { - st.load_reducer.reduce(LoadEvent::InstallFailed { - namespace, - identity, - error: LoadError { - code: LoadErrorCode::Install, - detail: message.clone(), - }, - candidate: None, - }); - st.post_event(&RendererEvent::LoadFailed { - namespace, - identity, - code: LoadErrorCode::Install, - detail: message, - }); - return; - } - }; + st.scene_generation = st.scene_generation.saturating_add(1); + let scene_owner = owner(st.load_epoch, st.scene_generation); + let (scene_reservations, texture_reservations) = match admit_scene( + &st.ledger, + scene_owner, + &model, + ) { + Ok(reservations) => reservations, + Err(message) => { + st.load_reducer.reduce(LoadEvent::InstallFailed { + namespace, + identity, + error: LoadError { code: LoadErrorCode::Install, detail: message.clone() }, + candidate: None, + }); + st.post_event(&RendererEvent::LoadFailed { + namespace, + identity, + code: LoadErrorCode::Install, + detail: message, + }); + return; + } + }; let candidate = match build_scene(&renderer.context, &model, pw, ph) { Ok(candidate) => candidate, Err(detail) => { @@ -1194,280 +1167,6 @@ async fn load_mesh_async( // RenderPreviewImage (async) // --------------------------------------------------------------------------- -fn handle_compound_load( - state: &Rc>, - namespace: RendererNamespace, - identity: LoadIdentity, - plan: polymodel_renderer_protocol::CompoundLoadPlan, -) { - let state_clone = state.clone(); - spawn_local(async move { - let result = load_compound_mesh(&state_clone, identity, plan).await; - finish_loaded_mesh(&state_clone, namespace, identity, result); - }); -} - -async fn load_compound_mesh( - state: &Rc>, - identity: LoadIdentity, - plan: polymodel_renderer_protocol::CompoundLoadPlan, -) -> Result { - if plan.version != polymodel_renderer_protocol::PROTOCOL_VERSION { - return Err("compound plan protocol version mismatch".into()); - } - if plan.descriptors.is_empty() { - return Err("compound plan has no descriptors".into()); - } - let mut descriptors = HashMap::with_capacity(plan.descriptors.len()); - for descriptor in plan.descriptors { - descriptor.validate().map_err(str::to_owned)?; - if descriptors - .insert(descriptor.key.clone(), descriptor) - .is_some() - { - return Err("compound plan contains duplicate descriptor keys".into()); - } - } - let primary = descriptors - .get(&plan.primary) - .ok_or_else(|| "compound plan primary is missing".to_string())?; - let owner = { - let st = state.borrow(); - owner(identity.load_epoch, st.scene_generation) - }; - let total_bytes = descriptors.values().try_fold(0_u64, |total, descriptor| { - total - .checked_add(descriptor.byte_length) - .ok_or("compound plan byte bound overflow") - })?; - if total_bytes > MAX_UNKNOWN_FETCH_BYTES { - return Err("compound plan exceeds fetch bound".into()); - } - let reservation = { - let st = state.borrow(); - st.ledger - .composite( - owner, - &[ - (ResourceClass::NetworkInFlight, total_bytes), - (ResourceClass::RetainedFetchedBytes, total_bytes), - (ResourceClass::DecodedSourceData, total_bytes), - (ResourceClass::WasmPages, WASM_FIXED_BYTES), - ], - ) - .map_err(|error| format!("compound load admission: {error}"))? - }; - let mut bytes_by_key = HashMap::with_capacity(descriptors.len()); - for (key, descriptor) in &descriptors { - let bytes = fetch_descriptor_bytes( - &descriptor.route, - descriptor.byte_length, - &descriptor.sha256, - ) - .await?; - bytes_by_key.insert(key.clone(), bytes); - } - let primary_bytes = bytes_by_key - .remove(&primary.key) - .ok_or_else(|| "compound plan primary bytes are missing".to_string())?; - let mut companions = Vec::with_capacity(bytes_by_key.len()); - for (key, bytes) in bytes_by_key { - companions.push((key, bytes)); - } - let format = mesh_format_for_descriptor(primary); - let source = polymodel_mesh::parser::MeshSource { - format, - primary: &primary_bytes, - resources: polymodel_mesh::parser::MeshResources::new(&companions), - }; - let model = polymodel_mesh::parser::load_mesh(&source).map_err(|error| error.to_string()); - drop(reservation); - model -} - -async fn fetch_descriptor_bytes( - route: &str, - expected_length: u64, - expected_sha256: &[u8], -) -> Result, String> { - let bytes = fetch_bytes(route).await?; - let expected_size = i64::try_from(expected_length) - .map_err(|_| "compound descriptor byte length is too large".to_string())?; - polymodel_renderer_protocol::verify_part_file(&bytes, expected_size, Some(expected_sha256)) - .map_err(|error| format!("compound descriptor integrity check failed: {error}"))?; - Ok(bytes) -} - -fn mesh_format_for_descriptor( - descriptor: &polymodel_renderer_protocol::WorkerFetchDescriptor, -) -> MeshFormat { - if descriptor.mime_type.contains("stl") || descriptor.key.ends_with(".dat") { - MeshFormat::Stl - } else if descriptor.mime_type.contains("obj") || descriptor.key.ends_with(".obj") { - MeshFormat::Obj - } else if descriptor.mime_type.contains("3mf") || descriptor.key.ends_with(".3mf") { - MeshFormat::Threemf - } else { - MeshFormat::Gltf - } -} - -fn finish_loaded_mesh( - state: &Rc>, - namespace: RendererNamespace, - identity: LoadIdentity, - result: Result, -) { - let mut st = match state.try_borrow_mut() { - Ok(st) => st, - Err(_) => return, - }; - if st.disposed { - return; - } - let model = match result { - Ok(model) => model, - Err(detail) => { - let error = LoadError { - code: LoadErrorCode::Fetch, - detail: detail.clone(), - }; - if matches!( - st.load_reducer - .reduce(LoadEvent::LoadFailed { - namespace, - identity, - error, - candidate: None - }) - .outcome, - polymodel_renderer_protocol::TransitionOutcome::Committed - ) { - st.post_event(&RendererEvent::LoadFailed { - namespace, - identity, - code: LoadErrorCode::Fetch, - detail, - }); - } - return; - } - }; - let parsed = st.load_reducer.reduce(LoadEvent::LoadSucceeded { - namespace, - identity, - candidate: Some(ResourceToken(identity.scene_generation as u64)), - }); - if !matches!( - parsed.outcome, - polymodel_renderer_protocol::TransitionOutcome::Committed - ) { - return; - } - st.post_event(&RendererEvent::LoadSucceeded { - namespace, - identity, - }); - let (pw, ph) = (st.pixel_width, st.pixel_height); - st.scene_generation = st.scene_generation.saturating_add(1); - let scene_owner = owner(st.load_epoch, st.scene_generation); - let Some(renderer) = st.renderer.as_ref() else { - let error = LoadError { - code: LoadErrorCode::Renderer, - detail: "renderer context unavailable".into(), - }; - st.load_reducer.reduce(LoadEvent::InstallFailed { - namespace, - identity, - error: error.clone(), - candidate: None, - }); - st.post_event(&RendererEvent::LoadFailed { - namespace, - identity, - code: error.code, - detail: error.detail, - }); - return; - }; - let (scene_reservations, texture_reservations) = - match admit_scene(&st.ledger, scene_owner, &model) { - Ok(reservations) => reservations, - Err(message) => { - st.load_reducer.reduce(LoadEvent::InstallFailed { - namespace, - identity, - error: LoadError { - code: LoadErrorCode::Install, - detail: message.clone(), - }, - candidate: None, - }); - st.post_event(&RendererEvent::LoadFailed { - namespace, - identity, - code: LoadErrorCode::Install, - detail: message, - }); - return; - } - }; - let candidate = match build_scene(&renderer.context, &model, pw, ph) { - Ok(candidate) => candidate, - Err(detail) => { - drop(scene_reservations); - drop(texture_reservations); - st.load_reducer.reduce(LoadEvent::InstallFailed { - namespace, - identity, - error: LoadError { - code: LoadErrorCode::Install, - detail: detail.clone(), - }, - candidate: None, - }); - st.post_event(&RendererEvent::LoadFailed { - namespace, - identity, - code: LoadErrorCode::Install, - detail, - }); - return; - } - }; - st.pending_scene = Some(candidate); - let committed = st.load_reducer.reduce(LoadEvent::InstallSucceeded { - namespace, - identity, - scene: Some(ResourceToken(identity.scene_generation as u64 + 1)), - }); - if !matches!( - committed.outcome, - polymodel_renderer_protocol::TransitionOutcome::Committed - ) { - st.pending_scene = None; - return; - } - if let Some((render_model, camera, control)) = st.pending_scene.take() { - if let Some(renderer) = st.renderer.as_mut() { - renderer.model = Some(render_model); - renderer.camera = camera; - renderer.control = control; - renderer.scene_reservations = Some(scene_reservations); - renderer.texture_reservations = texture_reservations; - } - } - st.needs_render = true; - st.post_event(&RendererEvent::InstallSucceeded { - namespace, - identity, - stats: MeshStats { - vertices: model.vertex_count(), - triangles: model.triangle_count(), - }, - }); -} - fn handle_render_preview_image( state: &Rc>, request_id: u32, @@ -1681,45 +1380,6 @@ fn handle_command(state: &Rc>, cmd: RendererCommand) { expected_digest, ); } - RendererCommand::StartCompoundLoad { - namespace, - identity, - plan, - } => { - if st.disposed || namespace != RendererNamespace::Interactive { - return; - } - let previous_identity = st.load_reducer.current_identity(); - let result = st.load_reducer.reduce(LoadEvent::StartLoad { - namespace, - identity, - }); - for _token in result.disposed { - st.pending_scene = None; - st.renderer.as_mut().map(|renderer| renderer.model = None); - } - if !matches!( - result.outcome, - polymodel_renderer_protocol::TransitionOutcome::Committed - ) { - return; - } - if let Some(renderer) = st.renderer.as_mut() { - renderer.model = None; - } - st.pending_scene = None; - st.needs_render = true; - if let Some(previous_identity) = previous_identity - && identity > previous_identity - { - st.post_event(&RendererEvent::LoadCancelled { - namespace, - identity: previous_identity, - }); - } - drop(st); - handle_compound_load(state, namespace, identity, plan); - } RendererCommand::Cancel { namespace, identity, -- 2.51.2