From 278f356aa604fa21c2d15d0081823bbe0f891df2 Mon Sep 17 00:00:00 2001 From: chexit Date: Wed, 4 Nov 2026 20:34:12 +0300 Subject: [PATCH] Scope camera picture milestones to the run whose frame drew them A camera shown only as the screen's own view was never prepared: the note required a non-empty picture list. Two runs picking the same local id both claimed the screen's frame, though only the first one's camera was drawn from it. The client now notes whether the frame was a world frame and the (run, camera id) the screen's world was drawn from, so the main-view milestone belongs to the run whose selection the screen used, and a title or loading frame reports the path off without erasing earlier milestones. A world transition clears the submission, the main-view selection and the milestones: another world's cameras are not drawn. A camera given to set-main-view drops its picture target, so its milestone starts over until a frame is drawn from it. prepared stays a host-side encoding milestone, not target registration, not a GPU fence, not a visible pixel. Also gate the client's note on the `mods` feature: without it the default horizon-client build did not compile. Verified: horizon-mods 37 unit, 44 sandbox (1 ignored; real Wasmtime guests, the two-run test shows each id 7 follows only its own pictures and selection) and 4 api_reference; client checks and build with and without `mods`; gfx lib tests. Co-Authored-By: Kimi Code --- crates/horizon-client/src/game.rs | 9 +- crates/horizon-mod-sdk/wit/horizon.wit | 31 +-- crates/horizon-mods/src/runtime.rs | 94 +++++---- crates/horizon-mods/tests/sandbox.rs | 184 +++++++++++++----- crates/horizon-mods/tests/ui_guest/src/lib.rs | 74 ++++--- docs/api/mods-reference.md | 6 +- 6 files changed, 272 insertions(+), 126 deletions(-) diff --git a/crates/horizon-client/src/game.rs b/crates/horizon-client/src/game.rs index 0881fde41..c0f38dfec 100644 --- a/crates/horizon-client/src/game.rs +++ b/crates/horizon-client/src/game.rs @@ -12154,12 +12154,15 @@ impl Game<'_> { } None => Err("renderer".into()), }; - // AirDef fork: the camera-ready milestone of the frame just encoded (not GPU completion). + // AirDef fork: the camera picture milestones of the frame just encoded (not GPU + // completion). Only a world frame draws mod pictures; the screen's own view has no + // picture target, so the (run, camera id) it was drawn from is a milestone too. + #[cfg(feature = "mods")] if let (Ok(true), Some(renderer)) = (&result, &self.renderer) { - let enabled = !frame.title && !frame.mod_views.is_empty(); + let main_view = main_view.as_ref().map(|v| (v.owner, v.id)); self.mods .mods - .note_camera_frame(enabled, renderer.drawn_mod_views()); + .note_camera_frame(!frame.title, main_view, renderer.drawn_mod_views()); } // Restore storage even when rendering fails (for example, a lost surface). self.entity_vertices = entities; diff --git a/crates/horizon-mod-sdk/wit/horizon.wit b/crates/horizon-mod-sdk/wit/horizon.wit index d390f5598..586feb225 100644 --- a/crates/horizon-mod-sdk/wit/horizon.wit +++ b/crates/horizon-mod-sdk/wit/horizon.wit @@ -902,7 +902,9 @@ interface render { /// lighting, sky, clouds, fog, smoke, TAA and the post passes, at the /// window's resolution. The picture size of that camera is ignored, the /// player's hand, block outline and the game's HUD are not drawn (the mod's - /// own `set-hud` shapes are); a `view` shape of that camera draws nothing. + /// own `set-hud` shapes are); a `view` shape of that camera draws nothing, + /// its picture target is dropped, and `camera-status` counts it prepared + /// again only after a frame is drawn from it. /// For a full-screen replay or a director's camera; the small pictures of /// monitors stay `view` shapes. None (or an unknown id) is the player's /// own view. The chunks stay those round the player. @@ -1054,10 +1056,11 @@ interface render { /// One `set-views` camera's readiness in the calling run and world generation. /// `request-accepted` means the run's latest non-empty `set-views` stored this /// camera this generation. `prepared` means the renderer encoded this camera's - /// picture into its texture target in a real frame of this generation (or the - /// camera is the run's `set-main-view` selection and a world frame rendered - /// from it): a host-side encoding milestone, not GPU completion, not a visible - /// pixel. A rotating per-frame budget may skip a picture camera some frames. + /// picture into its texture target in a real frame of this generation, or a + /// world frame was drawn from it as this run's `set-main-view` selection (only + /// the run whose selection the screen used gets that milestone): a host-side + /// encoding milestone, not GPU completion, not a visible pixel. A rotating + /// per-frame budget may skip a picture camera some frames. record camera-readiness { id: u32, request-accepted: bool, @@ -1069,14 +1072,16 @@ interface render { ready, } /// API level 30. State of the calling run's `set-views` cameras for this world. - /// Available means this host implements the API. Enabled means the renderer's - /// mod-camera picture path was live in the last noted frame (a world frame, not - /// a title screen), not that any camera was drawn. `request-accepted` and - /// `prepared` aggregate the per-camera list: accepted means the run's latest - /// `set-views` stored cameras this generation, prepared means at least one was - /// drawn (or rendered as the screen's main view). `main-view` is the camera id - /// the run last selected with `set-main-view`, if any. An empty `set-views` - /// clears the request; a world transition or mod reload invalidates readiness. + /// Available means this host implements the API. Enabled means the last noted + /// frame was a world frame, where the renderer draws the mods' pictures (not the + /// title or a loading screen), not that any camera was drawn. `request-accepted` + /// and `prepared` aggregate the per-camera list: accepted means the run's latest + /// `set-views` stored cameras this generation, prepared means at least one has a + /// milestone. `main-view` is the camera id the run last selected with + /// `set-main-view`, if any; when runs select ids that meet, the screen uses the + /// first running mod's. An empty `set-views` clears the request; a world + /// transition clears the submission and the main-view selection, and a mod + /// reload starts over. record camera-state { generation: u64, available: bool, diff --git a/crates/horizon-mods/src/runtime.rs b/crates/horizon-mods/src/runtime.rs index fc54d00fb..4a6c5371b 100644 --- a/crates/horizon-mods/src/runtime.rs +++ b/crates/horizon-mods/src/runtime.rs @@ -321,8 +321,7 @@ pub struct ModelTable { smoke_world: Option<(String, Option)>, /// The renderer draws the mods' spot lights and their beams (`render.set-spot-lights`). spot_active: bool, - /// `render.set-views`: the last noted frame's mod-camera picture path state. - camera_frame_seen: bool, + /// `render.set-views`: the last noted frame was a world frame, where pictures are drawn. camera_enabled: bool, point_lights_frame_seen: bool, point_lights_enabled: bool, @@ -974,7 +973,6 @@ impl Mods { t.smoke_frame_seen = false; t.point_lights_frame_seen = false; t.point_lights_enabled = false; - t.camera_frame_seen = false; t.camera_enabled = false; t.source_output_seen = false; t.source_output_ready = false; @@ -1000,6 +998,11 @@ impl Mods { state.smoke.clear(); state.smoke_clock = (0.0, 0.0); state.smoke_request_generation = None; + // The cameras of another world are not drawn in this one: the + // submission and the screen's selection start over, as an empty + // `set-views` does. + state.views.clear(); + state.main_view = None; state.views_request_generation = None; state.views_prepared.clear(); } @@ -1714,33 +1717,52 @@ impl Mods { } } } - /// State of the last frame's mod-camera picture path, as the game noted it - /// after a rendered frame. `enabled` means the picture path was live (a world - /// frame with mod cameras, not a title screen); `drawn` lists the (run, camera - /// id) pictures the renderer encoded this frame. This is a host-side encoding - /// milestone, not GPU completion. - pub fn note_camera_frame(&mut self, enabled: bool, drawn: &[(u64, u32)]) { - let generation = if let Ok(mut t) = self.model_table.lock() { - t.camera_frame_seen = t.smoke_world.is_some(); - t.camera_enabled = enabled && t.camera_frame_seen; - t.smoke_generation + /// The frame the game just drew, as it noted it after a rendered frame. + /// `world` means a world frame (not the title or a loading screen, where no + /// mod picture is drawn); `main_view` is the (run, camera id) the screen's + /// world was drawn from, if any; `drawn` lists the (run, camera id) pictures + /// the renderer encoded into their targets this frame. These are host-side + /// encoding milestones, not GPU completion. + pub fn note_camera_frame( + &mut self, + world: bool, + main_view: Option<(u64, u32)>, + drawn: &[(u64, u32)], + ) { + let (generation, live) = if let Ok(mut t) = self.model_table.lock() { + t.camera_enabled = world && t.smoke_world.is_some(); + (t.smoke_generation, t.camera_enabled) } else { return; }; + if !live { + return; + } for entry in &mut self.entries { - if let Some(run) = &mut entry.run { - let state = run.store.data_mut(); - if state.views_request_generation != Some(generation) { - continue; - } - for &(_, id) in drawn - .iter() - .filter(|(serial, _)| *serial == state.run_serial) - { - if state.views.iter().any(|v| v.id == id) && !state.views_prepared.contains(&id) - { - state.views_prepared.push(id); - } + let Some(run) = &mut entry.run else { + continue; + }; + let state = run.store.data_mut(); + if state.views_request_generation != Some(generation) { + continue; + } + let mut ids: Vec = drawn + .iter() + .filter(|(serial, _)| *serial == state.run_serial) + .map(|&(_, id)| id) + .collect(); + // The screen's own view is that camera's frame milestone: it has no + // picture target of its own. Only the run whose selection the screen + // used gets it; another run's pick of the same id is not prepared. + if let Some(&(_, id)) = main_view + .as_ref() + .filter(|(serial, _)| *serial == state.run_serial) + { + ids.push(id); + } + for id in ids { + if state.views.iter().any(|v| v.id == id) && !state.views_prepared.contains(&id) { + state.views_prepared.push(id); } } } @@ -2904,6 +2926,11 @@ impl render::Host for State { }; } fn set_main_view(&mut self, view: Option) { + // A camera the screen's world is drawn from has no picture target: the + // renderer drops it, so that id's picture milestone starts over. + if let Some(id) = view { + self.views_prepared.retain(|v| *v != id); + } self.main_view = view; } fn set_lights(&mut self, lights: Vec) { @@ -3299,15 +3326,14 @@ impl render::Host for State { }; let accepted = self.views_request_generation == Some(t.smoke_generation) && t.smoke_generation != 0; - let main_prepared = t.camera_enabled; let cameras: Vec = if accepted { self.views .iter() .map(|v| { - // The main view is the screen itself: a world frame rendered from - // it is its milestone, with no picture target of its own. - let prepared = self.views_prepared.contains(&v.id) - || (main_prepared && self.main_view == Some(v.id)); + // A drawn picture of this run's camera, or a world frame drawn + // from it as this run's `set-main-view` selection (noted for the + // run whose selection the screen used). + let prepared = self.views_prepared.contains(&v.id); render::CameraReadiness { id: v.id, request_accepted: true, @@ -3328,11 +3354,7 @@ impl render::Host for State { render::CapabilityReason::AwaitingWorld } else if !accepted { render::CapabilityReason::NotRequested - } else if !t.camera_frame_seen { - render::CapabilityReason::AwaitingBackend - } else if !t.camera_enabled { - render::CapabilityReason::Disabled - } else if !prepared { + } else if !t.camera_enabled || !prepared { render::CapabilityReason::AwaitingBackend } else { render::CapabilityReason::Ready diff --git a/crates/horizon-mods/tests/sandbox.rs b/crates/horizon-mods/tests/sandbox.rs index 120b6fd05..cabf6f7af 100644 --- a/crates/horizon-mods/tests/sandbox.rs +++ b/crates/horizon-mods/tests/sandbox.rs @@ -297,8 +297,9 @@ fn point_lights_milestones_follow_processing_and_world_generation() { assert!(status(&mut mods).ends_with("false false false CapabilityReason::AwaitingWorld")); } -/// The picture milestone is a drawn frame, not a stored target; a resubmission, -/// a world transition or a reload waits for a new frame. +/// The picture milestone is a drawn picture or a world frame from the run's own +/// main view, not a stored target; a resubmission, a clear, a world transition or +/// a reload waits for a new frame. #[test] fn camera_status_milestones_follow_drawn_frames_and_generation() { let dir = mods_dir().join("camera_status_reload"); @@ -332,14 +333,20 @@ fn camera_status_milestones_follow_drawn_frames_and_generation() { status(&mut mods) .ends_with("false true false CapabilityReason::AwaitingBackend main=None [7:false:CameraReason::AwaitingFrame]") ); - mods.note_camera_frame(true, &[]); - // A frame without this camera's picture: still awaiting. + mods.note_camera_frame(true, None, &[]); + // A world frame without this camera's picture: still awaiting. assert!( status(&mut mods) .ends_with("true true false CapabilityReason::AwaitingBackend main=None [7:false:CameraReason::AwaitingFrame]") ); let serial = mods.views().next().unwrap().0; - mods.note_camera_frame(true, &[(serial, 7)]); + // Another run's picture with the same local id is not this run's milestone. + mods.note_camera_frame(true, None, &[(serial ^ 1, 7)]); + assert!( + status(&mut mods) + .ends_with("true true false CapabilityReason::AwaitingBackend main=None [7:false:CameraReason::AwaitingFrame]") + ); + mods.note_camera_frame(true, None, &[(serial, 7)]); assert!(status(&mut mods).ends_with( "true true true CapabilityReason::Ready main=None [7:true:CameraReason::Ready]" )); @@ -350,16 +357,46 @@ fn camera_status_milestones_follow_drawn_frames_and_generation() { ); // The screen's own view from this camera is its milestone, without a picture target. assert!(mods.command("view_main on")); - mods.note_camera_frame(true, &[]); + // The screen consumes the camera: its picture target is gone until a frame. + assert!( + status(&mut mods) + .ends_with("true true false CapabilityReason::AwaitingBackend main=Some(7) [7:false:CameraReason::AwaitingFrame]") + ); + mods.note_camera_frame(true, Some((serial, 7)), &[]); assert!(status(&mut mods).ends_with( "true true true CapabilityReason::Ready main=Some(7) [7:true:CameraReason::Ready]" )); - assert!(mods.command("view_main off")); - mods.note_camera_frame(false, &[]); + // Another run's selection of the same id is not: the screen drew this run's camera. + assert!(mods.command("view_set")); + mods.note_camera_frame(true, Some((serial ^ 1, 7)), &[]); assert!( status(&mut mods) - .ends_with("false true false CapabilityReason::Disabled main=None [7:false:CameraReason::AwaitingFrame]") + .ends_with("true true false CapabilityReason::AwaitingBackend main=Some(7) [7:false:CameraReason::AwaitingFrame]") ); + // A title frame draws no picture: enabled goes false, the milestone stays. + mods.note_camera_frame(true, Some((serial, 7)), &[]); + mods.note_camera_frame(false, None, &[]); + assert!( + status(&mut mods) + .ends_with("false true true CapabilityReason::AwaitingBackend main=Some(7) [7:true:CameraReason::Ready]") + ); + mods.note_camera_frame(true, None, &[]); // a world frame again + assert!(status(&mut mods).ends_with( + "true true true CapabilityReason::Ready main=Some(7) [7:true:CameraReason::Ready]" + )); + // An empty submission clears the request and its milestones; a frame adds none. + assert!(mods.command("view_clear")); + assert!( + status(&mut mods) + .ends_with("true false false CapabilityReason::NotRequested main=Some(7) []") + ); + mods.note_camera_frame(true, Some((serial, 7)), &[(serial, 7)]); + assert!( + status(&mut mods) + .ends_with("true false false CapabilityReason::NotRequested main=Some(7) []") + ); + assert!(mods.command("view_main off")); + assert!(mods.command("view_set")); let old = status(&mut mods) .split_whitespace() .next() @@ -369,9 +406,11 @@ fn camera_status_milestones_follow_drawn_frames_and_generation() { let next = status(&mut mods); assert_ne!(next.split_whitespace().next().unwrap(), old); assert!(next.ends_with("false false false CapabilityReason::NotRequested main=None []")); + // Another world does not draw the old world's cameras: the host drops them. + assert_eq!(mods.views().count(), 0); assert!(mods.command("view_set")); let serial = mods.views().next().unwrap().0; - mods.note_camera_frame(true, &[(serial, 7)]); + mods.note_camera_frame(true, None, &[(serial, 7)]); assert!(status(&mut mods).ends_with( "true true true CapabilityReason::Ready main=None [7:true:CameraReason::Ready]" )); @@ -394,8 +433,8 @@ fn camera_status_milestones_follow_drawn_frames_and_generation() { ); } -/// Two runs share the local camera id 7; each run's status reflects only its -/// own submission and main-view choice. +/// Two runs share the local camera id 7; each run's milestones follow only its +/// own pictures and its own main-view selection, not the other run's. #[test] fn camera_status_is_scoped_to_each_mod_run() { let original = Package::read(&mods_dir().join("horizon_mods_test_ui_guest.hmod")) @@ -412,11 +451,12 @@ fn camera_status_is_scoped_to_each_mod_run() { .replace("Test UI Guest", "Other UI Guest"); std::fs::write(&two, Package::write(&other, &original).unwrap()).unwrap(); let mut mods = Mods::load(&[one, two], |_| true, |_| false); - mods.set_snapshot(Snapshot { - world: Some("test".into()), + let world = |name: &str| Snapshot { + world: Some(name.into()), world_seed: Some(-987_654_321), ..Snapshot::default() - }); + }; + mods.set_snapshot(world("test")); let lines = |mods: &mut Mods| { mods.drain() .into_iter() @@ -426,22 +466,28 @@ fn camera_status_is_scoped_to_each_mod_run() { }) .collect::>() }; - // "camera enabled accepted prepared generation reason". + // "camera enabled accepted prepared generation reason [id:prepared:reason]". fn fields(line: &str) -> (&str, &str, &str, &str) { let f: Vec<&str> = line.split_whitespace().collect(); assert_eq!(f.first(), Some(&"camera"), "{line}"); (f[1], f[2], f[3], f[5]) } + let ready = ("true", "true", "true", "CapabilityReason::Ready"); + let awaiting = ("true", "true", "false", "CapabilityReason::AwaitingBackend"); mods.tick(1); let runs: Vec<_> = mods.views().map(|(run, view)| (run, view.id)).collect(); assert_eq!(runs.len(), 2, "{runs:?}"); + assert_eq!(runs[0].1, 7, "{runs:?}"); + assert_eq!(runs[1].1, 7, "{runs:?}"); assert_ne!(runs[0].0, runs[1].0); - assert_eq!(runs[0].1, 7); - let mut first = lines(&mut mods); - first.sort(); - assert_eq!(first.len(), 2, "{first:?}"); + let (first, second) = (runs[0].0, runs[1].0); + // Both select id 7; the screen draws from the first running mod's. + assert_eq!(mods.owned_main_view(), Some((first, 7))); + let mut submitted = lines(&mut mods); + submitted.sort(); + assert_eq!(submitted.len(), 2, "{submitted:?}"); // No frame noted yet: accepted, not prepared, in this world generation. - for (_, line) in &first { + for (_, line) in &submitted { assert_eq!( fields(line), ( @@ -452,40 +498,50 @@ fn camera_status_is_scoped_to_each_mod_run() { ), "{line}" ); - } - mods.note_camera_frame(true, &[]); // a world frame: both render their main view - mods.tick(1); - let mut both = lines(&mut mods); - both.sort(); - assert_eq!(both.len(), 2, "{both:?}"); - for (_, line) in &both { - assert_eq!( - fields(line), - ("true", "true", "true", "CapabilityReason::Ready"), + assert!( + line.ends_with("[7:false:CameraReason::AwaitingFrame]"), "{line}" ); } - // Only the first run drops its main view: same local id, different readiness. - assert!(mods.command("view_main off")); - mods.note_camera_frame(true, &[]); - mods.tick(1); + // A world frame from the screen's view is that run's camera's milestone only. + mods.note_camera_frame(true, Some((first, 7)), &[]); + mods.tick(2); let split = lines(&mut mods); assert_eq!(split.len(), 2, "{split:?}"); for (name, line) in &split { - let ready = fields(line) == ("true", "true", "true", "CapabilityReason::Ready"); - assert_eq!(ready, *name != "Test UI Guest", "{name}: {line}"); + let f: Vec<&str> = line.split_whitespace().collect(); + if *name == "Test UI Guest" { + assert_eq!(fields(line), ready, "{name}: {line}"); + assert_eq!(f.last(), Some(&"[7:true:CameraReason::Ready]"), "{line}"); + } else { + // The screen did not draw from this run's id 7. + assert_eq!(fields(line), awaiting, "{name}: {line}"); + assert_eq!( + f.last(), + Some(&"[7:false:CameraReason::AwaitingFrame]"), + "{line}" + ); + } } - mods.set_snapshot(Snapshot { - world: Some("test2".into()), - world_seed: Some(-987_654_321), - ..Snapshot::default() - }); - mods.tick(1); + // Its own picture with the same local id is its milestone, nobody else's. + mods.note_camera_frame(true, Some((first, 7)), &[(second, 7)]); + mods.tick(3); + let mut both = lines(&mut mods); + both.sort(); + assert_eq!(both.len(), 2, "{both:?}"); + for (name, line) in &both { + assert_eq!(fields(line), ready, "{name}: {line}"); + } + // A new world generation drops both submissions and main views; the guests + // submit again for it, and the old milestones do not carry over. + mods.set_snapshot(world("test2")); + assert_eq!(mods.views().count(), 0); + assert_eq!(mods.owned_main_view(), None); + mods.tick(4); let mut after = lines(&mut mods); after.sort(); assert_eq!(after.len(), 2, "{after:?}"); for (_, line) in &after { - // A new generation: the old frame's milestones do not carry over. assert_eq!( fields(line), ( @@ -496,6 +552,46 @@ fn camera_status_is_scoped_to_each_mod_run() { ), "{line}" ); + assert!( + line.ends_with("[7:false:CameraReason::AwaitingFrame]"), + "{line}" + ); + } + // Only the run whose camera the screen drew becomes ready again. + mods.note_camera_frame(true, Some((first, 7)), &[]); + mods.tick(5); + let split = lines(&mut mods); + assert_eq!(split.len(), 2, "{split:?}"); + for (name, line) in &split { + if *name == "Test UI Guest" { + assert_eq!(fields(line), ready, "{name}: {line}"); + } else { + assert_eq!(fields(line), awaiting, "{name}: {line}"); + } + } + // The first run drops its main view: the screen moves to the second run's own + // selection. Its earlier milestone holds; the other's camera is not drawn yet. + assert!(mods.command("view_main off")); + mods.tick(6); + assert_eq!(mods.owned_main_view(), Some((second, 7))); + let mut switched = lines(&mut mods); + switched.sort(); + assert_eq!(switched.len(), 2, "{switched:?}"); + for (name, line) in &switched { + if *name == "Test UI Guest" { + assert_eq!(fields(line), ready, "{name}: {line}"); + } else { + assert_eq!(fields(line), awaiting, "{name}: {line}"); + } + } + // Then a world frame from that camera is its milestone, and the first run's own + // picture is its own. + mods.note_camera_frame(true, Some((second, 7)), &[(first, 7)]); + mods.tick(7); + let moved = lines(&mut mods); + assert_eq!(moved.len(), 2, "{moved:?}"); + for (name, line) in &moved { + assert_eq!(fields(line), ready, "{name}: {line}"); } } diff --git a/crates/horizon-mods/tests/ui_guest/src/lib.rs b/crates/horizon-mods/tests/ui_guest/src/lib.rs index 4249d5ede..c6e8b2141 100644 --- a/crates/horizon-mods/tests/ui_guest/src/lib.rs +++ b/crates/horizon-mods/tests/ui_guest/src/lib.rs @@ -77,6 +77,9 @@ struct Guest { pending: Option<(i32, bool, String)>, /// Whether the test camera is the screen's main view (`/view_main`). main_on: bool, + /// The world generation the test camera was last submitted for (a new world + /// gets a new submission, as a real mod would). + views_generation: Option, } fn widgets() -> Vec { @@ -165,6 +168,7 @@ impl Mod for Guest { Guest { pending: None, main_on: true, + views_generation: None, }, hooks, )) @@ -714,36 +718,52 @@ impl Mod for Guest { } fn tick(&mut self, _tick: u64) { if game::world_seed() == Some(-987_654_321) { - render::set_views(&[render::CameraView { - id: 7, - x: 1.0, - y: 2.0, - z: 3.0, - yaw: 0.0, - pitch: 0.0, - fov: 70.0, - width: 64, - height: 64, - }]); - if self.main_on { - render::set_main_view(Some(7)); - } else { - render::set_main_view(None); + // A submission belongs to a world generation: a new world is submitted for + // again, a frame's milestones within one are left to be noted. + let generation = render::camera_status().generation; + if self.views_generation != Some(generation) { + self.views_generation = Some(generation); + render::set_views(&[render::CameraView { + id: 7, + x: 1.0, + y: 2.0, + z: 3.0, + yaw: 0.0, + pitch: 0.0, + fov: 70.0, + width: 64, + height: 64, + }]); + if self.main_on { + render::set_main_view(Some(7)); + } else { + render::set_main_view(None); + } + render::set_hud(&[Shape::View(render::ViewShape { + view: 7, + rect: Rect { + x: 0.0, + y: 0.0, + w: 64.0, + h: 64.0, + }, + mode: 0, + })]); } - render::set_hud(&[Shape::View(render::ViewShape { - view: 7, - rect: Rect { - x: 0.0, - y: 0.0, - w: 64.0, - h: 64.0, - }, - mode: 0, - })]); let s = render::camera_status(); + let cameras: Vec = s + .cameras + .iter() + .map(|c| format!("{}:{}:{:?}", c.id, c.prepared, c.reason)) + .collect(); chat::show(&format!( - "camera {} {} {} {} {:?}", - s.enabled, s.request_accepted, s.prepared, s.generation, s.reason + "camera {} {} {} {} {:?} [{}]", + s.enabled, + s.request_accepted, + s.prepared, + s.generation, + s.reason, + cameras.join(";") )); } if let Some((book, all, id)) = self.pending.take() { diff --git a/docs/api/mods-reference.md b/docs/api/mods-reference.md index 7ada47cad..365af3000 100644 --- a/docs/api/mods-reference.md +++ b/docs/api/mods-reference.md @@ -820,7 +820,7 @@ API level 27: state for the calling mod and current world generation. Accepted m ### record `camera-readiness` -One `set-views` camera's readiness in the calling run and world generation. `request-accepted` means the run's latest non-empty `set-views` stored this camera this generation. `prepared` means the renderer encoded this camera's picture into its texture target in a real frame of this generation (or the camera is the run's `set-main-view` selection and a world frame rendered from it): a host-side encoding milestone, not GPU completion, not a visible pixel. A rotating per-frame budget may skip a picture camera some frames. +One `set-views` camera's readiness in the calling run and world generation. `request-accepted` means the run's latest non-empty `set-views` stored this camera this generation. `prepared` means the renderer encoded this camera's picture into its texture target in a real frame of this generation, or a world frame was drawn from it as this run's `set-main-view` selection (only the run whose selection the screen used gets that milestone): a host-side encoding milestone, not GPU completion, not a visible pixel. A rotating per-frame budget may skip a picture camera some frames. | Field | Type | | | --- | --- | --- | @@ -831,7 +831,7 @@ One `set-views` camera's readiness in the calling run and world generation. `req ### record `camera-state` -API level 30. State of the calling run's `set-views` cameras for this world. Available means this host implements the API. Enabled means the renderer's mod-camera picture path was live in the last noted frame (a world frame, not a title screen), not that any camera was drawn. `request-accepted` and `prepared` aggregate the per-camera list: accepted means the run's latest `set-views` stored cameras this generation, prepared means at least one was drawn (or rendered as the screen's main view). `main-view` is the camera id the run last selected with `set-main-view`, if any. An empty `set-views` clears the request; a world transition or mod reload invalidates readiness. +API level 30. State of the calling run's `set-views` cameras for this world. Available means this host implements the API. Enabled means the last noted frame was a world frame, where the renderer draws the mods' pictures (not the title or a loading screen), not that any camera was drawn. `request-accepted` and `prepared` aggregate the per-camera list: accepted means the run's latest `set-views` stored cameras this generation, prepared means at least one has a milestone. `main-view` is the camera id the run last selected with `set-main-view`, if any; when runs select ids that meet, the screen uses the first running mod's. An empty `set-views` clears the request; a world transition clears the submission and the main-view selection, and a mod reload starts over. | Field | Type | | | --- | --- | --- | @@ -942,7 +942,7 @@ Where a capture is. | `clear-post` | `func()` | Removes this mod's post pass: the world is shown as the game draws it. | | `set-params` | `func(params: list)` | The pass's `frame.params` (up to 1024 floats, as 256 vec4), kept until replaced; call it once a tick (the pass gets the partial tick in `frame.time.y`). | | `set-views` | `func(views: list)` | Replaces the mod's cameras: up to 8. A `view` shape shows one; the game renders up to three a frame, in turn. | -| `set-main-view` | `func(view: option)` | Renders the screen's world from one of the `set-views` cameras (AirDef fork), with everything the player's own view has: the HD lighting, sky, clouds, fog, smoke, TAA and the post passes, at the window's resolution. The picture size of that camera is ignored, the player's hand, block outline and the game's HUD are not drawn (the mod's own `set-hud` shapes are); a `view` shape of that camera draws nothing. For a full-screen replay or a director's camera; the small pictures of monitors stay `view` shapes. None (or an unknown id) is the player's own view. The chunks stay those round the player. | +| `set-main-view` | `func(view: option)` | Renders the screen's world from one of the `set-views` cameras (AirDef fork), with everything the player's own view has: the HD lighting, sky, clouds, fog, smoke, TAA and the post passes, at the window's resolution. The picture size of that camera is ignored, the player's hand, block outline and the game's HUD are not drawn (the mod's own `set-hud` shapes are); a `view` shape of that camera draws nothing, its picture target is dropped, and `camera-status` counts it prepared again only after a frame is drawn from it. For a full-screen replay or a director's camera; the small pictures of monitors stay `view` shapes. None (or an unknown id) is the player's own view. The chunks stay those round the player. | | `set-lights` | `func(lights: list)` | Replaces the mod's lights: up to 512, interpolated per id between the last two calls. | | `set-lighting` | `func(lighting: lighting)` | Switches the HD lighting on or off for this mod's world, with its exposure and bloom; off, the world is drawn as the game draws it. | | `set-time-override` | `func(day-time: option)` | Shows the world at this time of day (AirDef fork): the sky, the sun and moon, the sky light and the fog, as `day-time` ticks (24000 a day, as `clock`). Only what the player sees changes: the world's own clock, its saves and its rules do not. None shows the world's own time. |