From 294a89929e3b92b4282259683b3e646c9d8cc590 Mon Sep 17 00:00:00 2001 From: Orual Date: Mon, 3 Aug 2026 08:50:40 -0400 Subject: [PATCH] PM-80: restore compound worker dispatch Restore the complete plan-based compound load path and route StartCompoundLoad through the worker reducer and loader. Epic: PM-86 Task: PM-80 --- .../polymodel-renderer-worker/src/worker.rs | 315 +++++++++++++++++- 1 file changed, 313 insertions(+), 2 deletions(-) diff --git a/crates/polymodel-renderer-worker/src/worker.rs b/crates/polymodel-renderer-worker/src/worker.rs index aae94da..6a20358 100644 --- a/crates/polymodel-renderer-worker/src/worker.rs +++ b/crates/polymodel-renderer-worker/src/worker.rs @@ -5,6 +5,7 @@ //! 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; @@ -1164,8 +1165,279 @@ 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>, @@ -1380,6 +1652,45 @@ 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