From eaef56ebce94739a0d53543d4f4b62f13b0b37f0 Mon Sep 17 00:00:00 2001 From: Stella Wang Date: Wed, 21 Oct 2026 07:18:41 -0400 Subject: [PATCH 1/3] feat(sky): a stated sky can stand at a tick and ask for the game's look StatedSky gains `tick: Option` (time, look, render distance). At a stated tick the environment is sampled at it, HQ mode weights the stated look through the one `look_weight` (`sky::day_weight` for `ByTime`) that `Sky::Vanilla` uses, and both modes draw the tick's sunrise glow, moon and stars through the one `tick_celestials` the vanilla arm uses. `QuickSkyColours::vanilla` is the vanilla arm's colour derivation, public so a caller can state Lumenwright's reading. At no stated tick (`StatedSky::new`) the frame is 0.2.0's. StatedSky and StatedTick are `#[non_exhaustive]` with constructors. Tests: a stated sky at tick T equals `Sky::Vanilla { time: Some(T) }` with the same sun, byte for byte; at no tick it equals 0.2.0 as published (a dev-dependency on lumenwright =0.2.0), byte for byte. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016ba8LkuKW2bd5C5HCDo7p2 --- Cargo.lock | 35 ++- crates/lumenwright-cli/Cargo.toml | 4 + crates/lumenwright-cli/tests/library.rs | 169 ++++++++++++- crates/lumenwright/src/lib.rs | 3 +- crates/lumenwright/src/request.rs | 322 ++++++++++++++++-------- 5 files changed, 414 insertions(+), 119 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2603427..89e9737 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -801,7 +801,7 @@ dependencies = [ "glam", "image", "log", - "lumenwright-kernel", + "lumenwright-kernel 0.2.0", "lz4_flex", "pollster", "rayon", @@ -811,6 +811,27 @@ dependencies = [ "zip", ] +[[package]] +name = "lumenwright" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb592d531200ef20675b4f31bbd23d1cfd1d75641a18d3bde96e6ca816bcd985" +dependencies = [ + "anyhow", + "bytemuck", + "fastnbt", + "flate2", + "glam", + "image", + "log", + "lumenwright-kernel 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lz4_flex", + "rayon", + "serde", + "serde_json", + "zip", +] + [[package]] name = "lumenwright-cli" version = "0.2.0" @@ -822,7 +843,8 @@ dependencies = [ "flate2", "image", "log", - "lumenwright", + "lumenwright 0.2.0", + "lumenwright 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "rayon", "serde", "serde_json", @@ -839,6 +861,15 @@ dependencies = [ "libm", ] +[[package]] +name = "lumenwright-kernel" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20bdeedc07a4ab436fb79f9efcc8bf023c3db92c8cd7871c2874c5c615b5fd11" +dependencies = [ + "bytemuck", +] + [[package]] name = "lz4_flex" version = "0.14.0" diff --git a/crates/lumenwright-cli/Cargo.toml b/crates/lumenwright-cli/Cargo.toml index 6c2bd7c..4ff97c5 100644 --- a/crates/lumenwright-cli/Cargo.toml +++ b/crates/lumenwright-cli/Cargo.toml @@ -44,3 +44,7 @@ default = ["gpu"] gpu = ["lumenwright/gpu"] # CUDA backend: needs the PTX from scripts/build-ptx.sh. cuda = ["lumenwright/cuda"] + +[dev-dependencies] +# 0.2.0 as published: the oracle a stated sky at no stated tick is held to. +lumenwright_020 = { package = "lumenwright", version = "=0.2.0", default-features = false } diff --git a/crates/lumenwright-cli/tests/library.rs b/crates/lumenwright-cli/tests/library.rs index aabeefa..399752c 100644 --- a/crates/lumenwright-cli/tests/library.rs +++ b/crates/lumenwright-cli/tests/library.rs @@ -12,8 +12,8 @@ use lumenwright::render::{BackendKind, Mode}; use lumenwright::scene::camera::Camera; use lumenwright::world::Dimension; use lumenwright::{ - ChunkRect, EntityPolicy, HqOptions, HqSkyModel, QuickOptions, Rendered, Request, RequestError, - Sky, WorldSource, + ChunkRect, EntityPolicy, HqOptions, HqSkyModel, QuickOptions, QuickSkyColours, Rendered, + Request, RequestError, Sky, SkyLook, StatedSky, StatedTick, WorldSource, }; use lumenwright_cli::fixture; use lumenwright_cli::jar; @@ -199,15 +199,15 @@ fn a_stated_sky_is_drawn_as_given() { let mut req = request(&world, &jar, Mode::Quick); // Look straight up: the frame is all sky. req.camera.pitch = -90.0; - req.sky = Sky::Stated(lumenwright::StatedSky { - quick: lumenwright::QuickSkyColours { + req.sky = Sky::Stated(lumenwright::StatedSky::new( + lumenwright::QuickSkyColours { sky: Vec3::new(1.0, 0.0, 0.0), fog: Vec3::new(1.0, 0.0, 0.0), sun_dir: Vec3::new(0.0, -1.0, 0.0), sky_factor: 1.0, }, - hq: sky::hq_sky(Vec3::Y, 1.5, 1.0), - }); + sky::hq_sky(Vec3::Y, 1.5, 1.0), + )); let out = lumenwright::render(&req).unwrap(); let red = out .rgba @@ -217,6 +217,163 @@ fn a_stated_sky_is_drawn_as_given() { assert_eq!(red, 192 * 108, "the stated sky colour fills the frame"); } +/// A stated sky standing at a tick is drawn as `Sky::Vanilla` at that `time` is, when the +/// caller states the colours and suns Lumenwright derives there +/// (`QuickSkyColours::vanilla`, `sky::hq_sky`): byte for byte on the CPU, in quick mode +/// (the sunrise glow, the moon and the stars of the tick) and in HQ mode under the game's +/// look and under the look by time of day (its weight, the sky light and the lens's sky). +/// The tick moves the frame: without it the same stated sky draws something else. +#[test] +fn a_stated_sky_at_a_tick_is_the_vanilla_sky_at_it() { + let jar = require_jar!(); + let dir = tmp("stated-tick"); + let world = dir.join("world"); + fixture::block_entity_gallery().write(&world).unwrap(); + let store = lumenwright::assets::load_resources(&jar, None, &[]).unwrap(); + let distance = (RADIUS * 16) as f32; + let forward = camera().basis().0; + // 12800: the sun just set, the glow up; 18000: midnight, the moon and the stars out. + for (time, mode, look) in [ + (12800, Mode::Quick, SkyLook::ByTime), + (18000, Mode::Quick, SkyLook::ByTime), + (12800, Mode::Hq, SkyLook::Game), + (12800, Mode::Hq, SkyLook::ByTime), + (18000, Mode::Hq, SkyLook::Game), + ] { + let env = lumenwright::assets::environment::sample( + &store, + "minecraft:overworld", + "minecraft:plains", + time, + ) + .expect("a 1.21.11 jar carries environment attributes"); + let sun = sky::celestial_direction(env.sun_angle); + let mut vanilla = request(&world, &jar, mode); + vanilla.sky = Sky::Vanilla { + time: Some(time), + sun_dir: Some(sun), + hq: HqSkyModel { + look, + render_distance: distance, + ..HqSkyModel::default() + }, + }; + let colours = QuickSkyColours::vanilla( + Some(&env), + "minecraft:plains", + time, + Some(sun), + forward, + distance, + ); + let untimed = StatedSky::new(colours, sky::hq_sky(sun, sky::DEFAULT_SUN_INTENSITY, 1.0)); + let mut tick = StatedTick::new(time, look); + tick.render_distance = distance; + let mut stated = vanilla; + stated.sky = Sky::Stated(untimed.at(tick)); + let mut bare = vanilla; + bare.sky = Sky::Stated(untimed); + let name = format!("{mode:?} {look:?} at {time}"); + let want = lumenwright::render(&vanilla).unwrap().rgba; + let got = lumenwright::render(&stated).unwrap().rgba; + let differing = want + .chunks(4) + .zip(got.chunks(4)) + .filter(|(a, b)| a != b) + .count(); + assert_eq!(differing, 0, "{name}: pixels differ from Sky::Vanilla's"); + assert_ne!( + lumenwright::render(&bare).unwrap().rgba, + want, + "{name}: the tick moved nothing" + ); + } +} + +/// A stated sky at no stated tick (`StatedSky::new`) draws what 0.2.0, which had no tick to +/// state, draws for the same request: byte for byte on the CPU in both modes, under a world +/// whose `DayTime` is midnight, where a tick's moon, stars or game look would show. +#[test] +fn a_stated_sky_at_no_tick_draws_as_0_2_0_did() { + let jar = require_jar!(); + let dir = tmp("stated-0-2-0"); + let world = dir.join("world"); + let mut fx = fixture::block_entity_gallery(); + fx.day_time = 18000; + fx.write(&world).unwrap(); + let quick = lumenwright::QuickSkyColours { + sky: Vec3::new(0.2, 0.3, 0.6), + fog: Vec3::new(0.7, 0.5, 0.4), + sun_dir: sky::sun_direction(10.0, 120.0), + sky_factor: 0.6, + }; + let hq = sky::hq_sky(quick.sun_dir, 1.4, 0.9); + let old_quick = lumenwright_020::QuickSkyColours { + sky: quick.sky, + fog: quick.fog, + sun_dir: quick.sun_dir, + sky_factor: quick.sky_factor, + }; + let old_hq = lumenwright_020::render::sky::HqSky { + sun_dir: hq.sun_dir, + sun_color: hq.sun_color, + sky_light: hq.sky_light, + }; + for mode in [Mode::Quick, Mode::Hq] { + let mut req = request(&world, &jar, mode); + req.sky = Sky::Stated(StatedSky::new(quick, hq)); + let cam = req.camera; + let old = lumenwright_020::Request { + world: lumenwright_020::WorldSource::Dir { + path: &world, + dimension: lumenwright_020::world::Dimension::Overworld, + }, + chunks: lumenwright_020::ChunkRect { + min: req.chunks.min, + max: req.chunks.max, + }, + jar: &jar, + unifont: req.unifont, + resource_packs: &[], + camera: lumenwright_020::scene::camera::Camera { + pos: cam.pos, + yaw: cam.yaw, + pitch: cam.pitch, + fov_y: cam.fov_y, + }, + mode: match mode { + Mode::Quick => lumenwright_020::render::Mode::Quick, + Mode::Hq => lumenwright_020::render::Mode::Hq, + }, + backend: lumenwright_020::render::BackendKind::Cpu, + width: req.width, + height: req.height, + entities: lumenwright_020::EntityPolicy::default(), + quick: lumenwright_020::QuickOptions { + ssaa: req.quick.ssaa, + draw_distance: req.quick.draw_distance, + ..lumenwright_020::QuickOptions::default() + }, + hq: lumenwright_020::HqOptions { + spp: req.hq.spp, + ..lumenwright_020::HqOptions::default() + }, + sky: lumenwright_020::Sky::Stated(lumenwright_020::StatedSky { + quick: old_quick, + hq: old_hq, + }), + }; + let want = lumenwright_020::render(&old).unwrap().rgba; + let got = lumenwright::render(&req).unwrap().rgba; + let differing = want + .chunks(4) + .zip(got.chunks(4)) + .filter(|(a, b)| a != b) + .count(); + assert_eq!(differing, 0, "{mode:?}: pixels differ from 0.2.0's"); + } +} + /// A JSON file from the client jar. fn jar_json(jar: &Path, name: &str) -> serde_json::Value { let f = std::fs::File::open(jar).unwrap(); diff --git a/crates/lumenwright/src/lib.rs b/crates/lumenwright/src/lib.rs index d4147fe..d5190fa 100644 --- a/crates/lumenwright/src/lib.rs +++ b/crates/lumenwright/src/lib.rs @@ -112,5 +112,6 @@ pub mod world; pub use request::{ ChunkRect, EmitterSampling, EntityPolicy, HqOptions, HqSkyModel, QuickOptions, QuickSkyColours, - Rendered, Request, RequestError, Sky, SkyLook, StatedSky, UnknownEntities, WorldSource, render, + Rendered, Request, RequestError, Sky, SkyLook, StatedSky, StatedTick, UnknownEntities, + WorldSource, render, }; diff --git a/crates/lumenwright/src/request.rs b/crates/lumenwright/src/request.rs index ff027ee..afdefb9 100644 --- a/crates/lumenwright/src/request.rs +++ b/crates/lumenwright/src/request.rs @@ -286,13 +286,66 @@ impl HqSkyModel { }; } -/// A caller-stated sky for both modes. +/// A caller-stated sky for both modes. Build it with [`StatedSky::new`]; the struct is +/// `#[non_exhaustive]`, so a field added later is not a breaking change. #[derive(Clone, Copy, Debug)] +#[non_exhaustive] pub struct StatedSky { /// The sky quick mode draws. pub quick: QuickSkyColours, - /// The sun and sky HQ mode traces. + /// The sun and sky HQ mode traces: Chunky's part of the frame. pub hq: HqSky, + /// The tick the sky stands at and the look HQ mode draws. `None` (the default): the + /// environment is sampled at the world's `DayTime`, quick mode draws the stated colours + /// and sun alone, and HQ mode traces [`StatedSky::hq`] alone, as Chunky's look. + pub tick: Option, +} + +impl StatedSky { + /// The stated colours and sun, at no stated tick ([`StatedSky::tick`] `None`). + pub fn new(quick: QuickSkyColours, hq: HqSky) -> StatedSky { + StatedSky { + quick, + hq, + tick: None, + } + } + + /// The same sky standing at `tick`. + pub fn at(self, tick: StatedTick) -> StatedSky { + StatedSky { + tick: Some(tick), + ..self + } + } +} + +/// The tick a [`StatedSky`] stands at, and the look HQ mode draws there. The stated colours +/// and suns stay the caller's; what the client derives from the tick alone is drawn from the +/// environment at it, exactly as under [`Sky::Vanilla`] at that `time`: the sunrise glow, +/// the moon and the stars in both modes, and under the game's look its sky light. +/// `#[non_exhaustive]`: build it with [`StatedTick::new`]. +#[derive(Clone, Copy, Debug, PartialEq)] +#[non_exhaustive] +pub struct StatedTick { + /// Ticks (0 = sunrise, 6000 = noon), not reduced to one day: the moon's phase spans eight. + pub time: i64, + /// Which look HQ mode draws, weighted as under [`Sky::Vanilla`] ([`SkyLook::ByTime`] + /// by [`sky::day_weight`] at `time`). [`StatedSky::hq`] is Chunky's part. + pub look: SkyLook, + /// The client's render distance in blocks, which the game's sky fog depends on. + pub render_distance: f32, +} + +impl StatedTick { + /// `look` at `time`, at the client's default render distance. + pub fn new(time: i64, look: SkyLook) -> StatedTick { + StatedTick { + time, + look, + render_distance: DEFAULT_RENDER_DISTANCE, + } + } } /// Quick mode's sky: the colours [`QuickSky`] draws and the lightmap's daylight. @@ -571,7 +624,7 @@ pub fn render(req: &Request) -> Result { }; let day_time = match req.sky { Sky::Vanilla { time, .. } => time.unwrap_or(world.level.day_time), - Sky::Stated(_) => world.level.day_time, + Sky::Stated(s) => s.tick.map_or(world.level.day_time, |t| t.time), }; let env = assets::environment::sample(&store, dimension, &biome, day_time); // The night and day values of `visual/sky_light_factor` over the day timeline. @@ -656,30 +709,17 @@ pub fn render(req: &Request) -> Result { Mode::Hq => { let hq = voxel::build_hq(&scene)?; lap("build voxel scene", &mut t); - // How much of the frame is Chunky's look: 1 for `Chunky` and a stated sky, 0 for - // `Game`, and by the time of day for `ByTime` (`sky::day_weight`). - let (model, weight) = match req.sky { - Sky::Stated(_) => (None, 1.0), - Sky::Vanilla { hq: m, .. } => { - let w = match m.look { - SkyLook::Chunky => 1.0, - SkyLook::Game => 0.0, - SkyLook::ByTime => match (&env, day_range) { - (Some(e), Some((night, day))) => { - sky::day_weight(e.sky_light_factor, night, day) - } - _ => { - log::warn!( - "no day timeline for `visual/sky_light_factor` in this jar: \ - drawing Chunky's sky" - ); - 1.0 - } - }, - }; - (Some(m), w) - } + // The look and the render distance its fog stands at: a stated sky at no stated + // tick is Chunky's. + let (look, render_distance) = match req.sky { + Sky::Vanilla { hq: m, .. } => (m.look, m.render_distance), + Sky::Stated(s) => s + .tick + .map_or((SkyLook::Chunky, DEFAULT_RENDER_DISTANCE), |t| { + (t.look, t.render_distance) + }), }; + let weight = look_weight(look, env.as_ref(), day_range); if weight < 1.0 && env.is_none() { anyhow::bail!( "the game's sky needs the environment attributes of a 1.21.11 or later client \ @@ -691,12 +731,13 @@ pub fn render(req: &Request) -> Result { weight * DEFAULT_EMITTER_INTENSITY + (1.0 - weight) * GAME_EMITTER_INTENSITY, ); // Chunky's sun and sky, for the Chunky part of the frame. - let chunky_sky = match (req.sky, model) { - (Sky::Stated(s), _) => s.hq, - (Sky::Vanilla { time, sun_dir, .. }, Some(m)) => { - sky::hq_sky(vanilla_sun(time, sun_dir), m.sun_intensity, m.sky_intensity) - } - (Sky::Vanilla { .. }, None) => unreachable!("a vanilla sky has a model"), + let chunky_sky = match req.sky { + Sky::Stated(s) => s.hq, + Sky::Vanilla { + time, + sun_dir, + hq: m, + } => sky::hq_sky(vanilla_sun(time, sun_dir), m.sun_intensity, m.sky_intensity), }; let mut params = hq_params( &req.hq, @@ -723,7 +764,6 @@ pub fn render(req: &Request) -> Result { sky::sky_cache(¶ms) } else { let e = env.as_ref().expect("checked above"); - let render_distance = model.map_or(DEFAULT_RENDER_DISTANCE, |m| m.render_distance); let rect = |loc: &str| { scene.atlas.id(loc).map(|id| { let s = scene.atlas.sprites[id as usize]; @@ -874,13 +914,37 @@ fn rgb(c: u32) -> Vec3 { Vec3::new(channel(c, 16), channel(c, 8), channel(c, 0)) } +/// How much of an HQ frame is Chunky's look: 1 for [`SkyLook::Chunky`], 0 for +/// [`SkyLook::Game`], and for [`SkyLook::ByTime`] where the environment's +/// `visual/sky_light_factor` stands between its night and day values ([`sky::day_weight`]), +/// or 1 for a jar with no day timeline. +fn look_weight( + look: SkyLook, + env: Option<&assets::environment::EnvironmentState>, + day_range: Option<(f32, f32)>, +) -> f32 { + match look { + SkyLook::Chunky => 1.0, + SkyLook::Game => 0.0, + SkyLook::ByTime => match (env, day_range) { + (Some(e), Some((night, day))) => sky::day_weight(e.sky_light_factor, night, day), + _ => { + log::warn!( + "no day timeline for `visual/sky_light_factor` in this jar: drawing Chunky's sky" + ); + 1.0 + } + }, + } +} + /// The quick sky for a frame: the lightmap's colours and what the sky passes draw. /// -/// For [`Sky::Vanilla`] with the jar's environment attributes, the fog colour is -/// `AtmosphericFogEnvironment.getBaseColor`: the fog attribute, blended towards -/// the sunrise colour by how directly the camera faces the sun's side of the sky -/// (render distance 4 chunks or more), then towards the sky colour by -/// `1 - lerp(min(sky fog end / 16, render distance) / 32, 0.25, 1)^0.25`. +/// Under [`Sky::Vanilla`] the colours are [`QuickSkyColours::vanilla`]'s; under +/// [`Sky::Stated`] the caller's. What the environment at the tick draws (the sunrise glow, +/// the moon and the stars, [`tick_celestials`]) is drawn under a vanilla sky whenever the +/// jar has environment attributes, and under a stated sky when it stands at a +/// [`StatedTick`]. #[allow(clippy::too_many_arguments)] fn quick_sky( req_sky: &Sky, @@ -893,94 +957,132 @@ fn quick_sky( sun_rect: Option<[f32; 4]>, rect: &dyn Fn(&str) -> Option<[f32; 4]>, ) -> (QuickSkyColours, sky::SkyDescription) { - use assets::environment::{Skybox, srgb_lerp}; + use assets::environment::Skybox; let env_fog = env.map_or((0.0, 1024.0), |e| (e.fog_start, e.fog_end)); let sky_fog_end = draw_distance.min(env.map_or(512.0, |e| e.sky_fog_end)); + // The colours, whether the caller placed the sun, and whether the tick's sky is drawn. + let (colours, sun_placed, at_tick) = match *req_sky { + Sky::Stated(s) => (s.quick, true, s.tick.is_some()), + Sky::Vanilla { time, sun_dir, .. } => ( + QuickSkyColours::vanilla( + env, + biome, + time.unwrap_or(world_day_time), + sun_dir, + forward, + draw_distance, + ), + sun_dir.is_some(), + true, + ), + }; let mut desc = sky::SkyDescription { - sky: Vec3::ZERO, - fog: Vec3::ZERO, + sky: colours.sky, + fog: colours.fog, sky_fog_end, render_fog, env_fog, sky_disc: env.is_none_or(|e| e.skybox == Skybox::Overworld), sunrise: [0.0; 4], sun_angle: 0.0, - sun: None, + sun: sun_rect.map(|r| (colours.sun_dir, r)), moon: None, star_angle: 0.0, star_brightness: 0.0, }; - match *req_sky { - Sky::Stated(s) => { - desc.sky = s.quick.sky; - desc.fog = s.quick.fog; - desc.sun = sun_rect.map(|r| (s.quick.sun_dir, r)); - (s.quick, desc) - } - Sky::Vanilla { time, sun_dir, .. } => { - let ticks = time.unwrap_or(world_day_time); - // The lightmap keeps its daylight factor on the pre-1.21.11 day curve. - let angle = sky::celestial_angle(ticks); - let day = sky::daylight(angle); - let Some(e) = env else { - let temp = assets::biome::biome_info(biome).temperature; - let sun = sun_dir.unwrap_or_else(|| sky::sun_direction_from_angle(angle)); - let colours = QuickSkyColours { - sky: Vec3::from(assets::biome::sky_color(temp)) * day, - fog: Vec3::new(0.753, 0.847, 1.0) * (day * 0.94 + 0.06), - sun_dir: sun, - sky_factor: day, - }; - desc.sky = colours.sky; - desc.fog = colours.fog; - desc.sun = sun_rect.map(|r| (sun, r)); - return (colours, desc); + if at_tick && let Some(e) = env { + tick_celestials(&mut desc, e, colours.sun_dir, sun_placed, rect); + } + (colours, desc) +} + +/// What the environment at a tick draws in the sky besides its colours and the sun: the +/// sunrise glow (`visual/sunrise_sunset_color`, on the side `visual/sun_angle` puts it), the +/// moon of `visual/moon_phase` and the stars at `visual/star_angle` and +/// `visual/star_brightness`. The moon stands opposite `sun` when the caller placed the sun, +/// else at `visual/moon_angle`. +fn tick_celestials( + desc: &mut sky::SkyDescription, + e: &assets::environment::EnvironmentState, + sun: Vec3, + sun_placed: bool, + rect: &dyn Fn(&str) -> Option<[f32; 4]>, +) { + let moon = if sun_placed { + -sun + } else { + sky::celestial_direction(e.moon_angle) + }; + let sr = e.sunrise_sunset_color; + desc.sunrise = [ + channel(sr, 16), + channel(sr, 8), + channel(sr, 0), + channel(sr, 24), + ]; + desc.sun_angle = e.sun_angle; + desc.moon = rect(&crate::scene::moon_texture(&e.moon_phase)).map(|r| (moon, r)); + desc.star_angle = e.star_angle; + desc.star_brightness = e.star_brightness; +} + +impl QuickSkyColours { + /// The colours [`Sky::Vanilla`] derives at `time` for a camera looking along `forward` + /// at a render distance of `draw_distance` blocks, from the environment attributes + /// sampled at the camera ([`assets::environment::sample`]): what a caller states to + /// draw Lumenwright's own reading of the client's sky through a [`StatedSky`]. + /// + /// With the jar's environment attributes, the fog colour is + /// `AtmosphericFogEnvironment.getBaseColor`: the fog attribute, blended towards + /// the sunrise colour by how directly the camera faces the sun's side of the sky + /// (render distance 4 chunks or more), then towards the sky colour by + /// `1 - lerp(min(sky fog end / 16, render distance) / 32, 0.25, 1)^0.25`. Without them + /// (`env` `None`, a jar before 1.21.11), the sky comes from the biome's temperature + /// and the old day curve. The lightmap keeps its daylight factor on the pre-1.21.11 + /// day curve either way. `sun_dir` `None` follows `time`. + pub fn vanilla( + env: Option<&assets::environment::EnvironmentState>, + biome: &str, + time: i64, + sun_dir: Option, + forward: Vec3, + draw_distance: f32, + ) -> QuickSkyColours { + use assets::environment::srgb_lerp; + let angle = sky::celestial_angle(time); + let day = sky::daylight(angle); + let Some(e) = env else { + let temp = assets::biome::biome_info(biome).temperature; + return QuickSkyColours { + sky: Vec3::from(assets::biome::sky_color(temp)) * day, + fog: Vec3::new(0.753, 0.847, 1.0) * (day * 0.94 + 0.06), + sun_dir: sun_dir.unwrap_or_else(|| sky::sun_direction_from_angle(angle)), + sky_factor: day, }; - let render_chunks = (draw_distance / 16.0).floor(); - let mut fog = e.fog_color; - if render_chunks >= 4.0 { - let side = if e.sun_angle.to_radians().sin() > 0.0 { - -1.0 - } else { - 1.0 - }; - let facing = forward.x * side; - let glow = e.sunrise_sunset_color >> 24; - if facing > 0.0 && glow > 0 { - let a = glow as f32 / 255.0; - fog = srgb_lerp(facing * a, fog, e.sunrise_sunset_color | 0xFF00_0000); - } - } - let reach = (e.sky_fog_end / 16.0).min(render_chunks); - let lerp = (reach / 32.0).clamp(0.0, 1.0) * 0.75 + 0.25; - fog = srgb_lerp(1.0 - lerp.powf(0.25), fog, e.sky_color); - let sun = sun_dir.unwrap_or_else(|| sky::celestial_direction(e.sun_angle)); - let moon = if sun_dir.is_some() { - -sun + }; + let render_chunks = (draw_distance / 16.0).floor(); + let mut fog = e.fog_color; + if render_chunks >= 4.0 { + let side = if e.sun_angle.to_radians().sin() > 0.0 { + -1.0 } else { - sky::celestial_direction(e.moon_angle) - }; - let sr = e.sunrise_sunset_color; - desc.sky = rgb(e.sky_color); - desc.fog = rgb(fog); - desc.sunrise = [ - channel(sr, 16), - channel(sr, 8), - channel(sr, 0), - channel(sr, 24), - ]; - desc.sun_angle = e.sun_angle; - desc.sun = sun_rect.map(|r| (sun, r)); - desc.moon = rect(&crate::scene::moon_texture(&e.moon_phase)).map(|r| (moon, r)); - desc.star_angle = e.star_angle; - desc.star_brightness = e.star_brightness; - let colours = QuickSkyColours { - sky: desc.sky, - fog: desc.fog, - sun_dir: sun, - sky_factor: day, + 1.0 }; - (colours, desc) + let facing = forward.x * side; + let glow = e.sunrise_sunset_color >> 24; + if facing > 0.0 && glow > 0 { + let a = glow as f32 / 255.0; + fog = srgb_lerp(facing * a, fog, e.sunrise_sunset_color | 0xFF00_0000); + } + } + let reach = (e.sky_fog_end / 16.0).min(render_chunks); + let lerp = (reach / 32.0).clamp(0.0, 1.0) * 0.75 + 0.25; + fog = srgb_lerp(1.0 - lerp.powf(0.25), fog, e.sky_color); + QuickSkyColours { + sky: rgb(e.sky_color), + fog: rgb(fog), + sun_dir: sun_dir.unwrap_or_else(|| sky::celestial_direction(e.sun_angle)), + sky_factor: day, } } } From 395b3b2f3197b3c9d2318ef5752551d4b6a634e7 Mon Sep 17 00:00:00 2001 From: Stella Wang Date: Wed, 21 Oct 2026 07:23:10 -0400 Subject: [PATCH 2/3] docs: a stated sky at a tick, in the changelog, the library guide and the tests CHANGELOG [Unreleased] records the stated tick, QuickSkyColours::vanilla, the non_exhaustive constructors (a breaking change for 0.2.x), and weather as not expressed: the game's look draws a clear sky. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016ba8LkuKW2bd5C5HCDo7p2 --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ crates/lumenwright/src/request.rs | 4 +++- docs/SCOPE.md | 2 +- docs/library.md | 10 ++++++++++ docs/testing.md | 2 +- 5 files changed, 41 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6245088..0600902 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,32 @@ All notable changes to Lumenwright are recorded here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow [Semantic Versioning](https://semver.org/). +## [Unreleased] + +### Added + +- A stated sky can stand at a tick and ask for the game's look: `StatedSky::tick` takes a + `StatedTick` (`time`, `look`, `render_distance`), set by `StatedSky::at`. At it the + environment is sampled at the tick, not at the world's `DayTime`; quick mode and the + game's look draw the tick's sunrise glow, moon and stars through the same code as + `Sky::Vanilla`; and HQ mode draws the stated look, weighted for `SkyLook::ByTime` by the + one `sky::day_weight`, with `StatedSky::hq` as Chunky's part. A stated sky at + `Sky::Vanilla`'s colours and sun draws its frame byte for byte. +- `QuickSkyColours::vanilla`: the colours `Sky::Vanilla` derives at a tick, for a caller to + state. + +### Changed + +- `StatedSky` and the new `StatedTick` are `#[non_exhaustive]`: build them with + `StatedSky::new(quick, hq)` and `StatedTick::new(time, look)`. A struct literal of + `StatedSky` no longer compiles, so this is a breaking change for 0.2.x. A stated sky at + no tick (`StatedSky::new`) draws what 0.2.0 drew, byte for byte. + +### Not expressed + +- Weather: the game's look, under `Sky::Vanilla` and at a stated tick, draws a clear sky; it + applies no rain or thunder. + ## [0.2.0] - 2026-10-21 ### Added diff --git a/crates/lumenwright/src/request.rs b/crates/lumenwright/src/request.rs index afdefb9..79f6b6a 100644 --- a/crates/lumenwright/src/request.rs +++ b/crates/lumenwright/src/request.rs @@ -207,7 +207,9 @@ pub enum Sky { /// What HQ mode lights the frame with and draws in its sky. hq: HqSkyModel, }, - /// The caller's sky, used as given; nothing is derived from the world's time. + /// The caller's colours and suns, used as given. At a stated tick + /// ([`StatedSky::tick`]) what the client derives from the tick alone is drawn from it; + /// otherwise nothing is derived from the world's time. Stated(StatedSky), } diff --git a/docs/SCOPE.md b/docs/SCOPE.md index ff5266b..e1f9282 100644 --- a/docs/SCOPE.md +++ b/docs/SCOPE.md @@ -18,7 +18,7 @@ and `lumenwright-cli`, the `lumenwright` command, which writes it. | G7 | **HQ mode**: looks like Chunky | A unidirectional path tracer over a sparse voxel grid, its algorithms and constants ported from Chunky ([docs/research/chunky-parity.md](research/chunky-parity.md)): the Preetham sky read through Chunky's sky cache, Chunky's sun with next-event estimation and drawn with the client's sun texture, Chunky's emitters (material emittance, quadratic emitted light, the emitter grid and its sampling strategies), one-sided block faces, multiple diffuse bounces, Fresnel water with refraction, stochastic translucency, the display gamma 2.2 with Chunky's gamma, ACES and Hable filters, progressive sample accumulation. | | G8 | Multiple native rendering backends | One `Backend` trait. **cpu**: Rust and rayon on all cores. **metal / vulkan / dx12**: native GPU APIs through `wgpu`, with WGSL shaders that naga translates to MSL, SPIR-V or HLSL. **cuda**: the same Rust path-tracing kernel crate that the CPU backend runs, compiled to PTX (`nvptx64-nvidia-cuda`) and launched through the CUDA driver API (`cudarc`, cargo feature `cuda`). | | G9 | Runnable Rust prototype, tested locally | Cargo workspace, unit and integration tests, a parity test between the CPU and GPU backends, and end-to-end renders of a real 1.21.11 save on this M2 Pro. | -| G10 | A library entry point for an embedding program | `lumenwright::render(&Request) -> Result` (`src/request.rs`). The request carries every input: the world or an opened `World`, an inclusive `ChunkRect`, the jar and resource-pack paths, the camera, mode, backend, size, `QuickOptions`, `HqOptions`, `EntityPolicy`, and a `Sky` that is either derived from a time of day or stated by the caller (quick colours and daylight factor, HQ sun and sky). It resolves no jar, performs no network access, writes no file and prints nothing. It returns RGBA8 pixels, the HQ radiance, the backend and the reason `auto` fell back to the CPU, stage timings, loaded counts and the per-kind `EntityStats` and `BlockEntityStats`. A refusal before drawing is a `RequestError`. | +| G10 | A library entry point for an embedding program | `lumenwright::render(&Request) -> Result` (`src/request.rs`). The request carries every input: the world or an opened `World`, an inclusive `ChunkRect`, the jar and resource-pack paths, the camera, mode, backend, size, `QuickOptions`, `HqOptions`, `EntityPolicy`, and a `Sky` that is either derived from a time of day or stated by the caller (quick colours and daylight factor, HQ sun and sky, and optionally the tick it stands at and the HQ look drawn there). It resolves no jar, performs no network access, writes no file and prints nothing. It returns RGBA8 pixels, the HQ radiance, the backend and the reason `auto` fell back to the CPU, stage timings, loaded counts and the per-kind `EntityStats` and `BlockEntityStats`. A refusal before drawing is a `RequestError`. | ## 2. In scope for the prototype diff --git a/docs/library.md b/docs/library.md index ebf075e..4d8899d 100644 --- a/docs/library.md +++ b/docs/library.md @@ -28,6 +28,16 @@ backend, the size, the quick and HQ options, which entities to draw, and the sky either vanilla's at a time of day (`Sky::Vanilla`) or the colours and sun you state (`Sky::Stated`). +A stated sky (`StatedSky::new(quick, hq)`) is drawn as given: quick mode draws your colours +and sun, HQ mode traces your Chunky sun and sky, and the environment is sampled at the +world's `DayTime`. Stand it at a tick (`.at(StatedTick::new(time, look))`) and what the +client derives from the tick alone is drawn from the environment there, as `Sky::Vanilla` +at that `time` draws it: the sunrise glow, the moon and the stars in both modes, and in HQ +mode the look you ask for (`SkyLook::Game`, `ByTime` or `Chunky`), with your `hq` as its +Chunky part. `QuickSkyColours::vanilla` gives the colours `Sky::Vanilla` derives, so a +stated sky built from them and `sky::hq_sky` at the same sun draws the vanilla frame byte +for byte. Weather is not expressed: the game's look draws a clear sky. + ```rust use std::path::Path; use lumenwright::glam::DVec3; diff --git a/docs/testing.md b/docs/testing.md index 104cd2e..7ad2e6a 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -26,7 +26,7 @@ cargo test --workspace --release parity in both modes (showcase, block-entity gallery and item gallery) and CPU-vs-CUDA parity in HQ mode, determinism, and the CLI. The library tests check that the CLI's PNG and the library's pixels are byte-equal in both modes, that a missing jar is refused by - name, that a stated sky is drawn as given, that the vanilla sky's colour at noon, dusk and + name, that a stated sky is drawn as given, that a stated sky at a tick draws `Sky::Vanilla`'s frame at it byte for byte (quick, and HQ under the game's look and by time), that a stated sky at no tick draws 0.2.0's frame byte for byte (0.2.0 as published is a dev-dependency), that the vanilla sky's colour at noon, dusk and midnight matches the jar's day timeline within 2/255, and that CPU pixels are bit-identical on 1, 3 and 8 threads (showcase and item gallery). A far-plane test measures the high-frequency energy and the change under a sub-pixel camera move with and without From 25e104252895f2192449ccd0fc05f68ff06f1bb2 Mon Sep 17 00:00:00 2001 From: Stella Wang Date: Wed, 21 Oct 2026 07:27:40 -0400 Subject: [PATCH 3/3] test: the 0.2.0 oracle lives in an unpublished crate, lumenwright-compat `cargo publish --dry-run --workspace` refused lumenwright-cli: a published crate may not depend on lumenwright 0.2.0 twice under two names (the path dependency and the =0.2.0 oracle). The oracle test moves to `crates/lumenwright-compat` (publish = false), a workspace member that `cargo test --workspace` runs and the package job skips. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016ba8LkuKW2bd5C5HCDo7p2 --- Cargo.lock | 10 +- Cargo.toml | 2 +- crates/lumenwright-cli/Cargo.toml | 4 - crates/lumenwright-cli/tests/library.rs | 84 -------- crates/lumenwright-compat/Cargo.toml | 18 ++ crates/lumenwright-compat/src/lib.rs | 4 + crates/lumenwright-compat/tests/stated_sky.rs | 192 ++++++++++++++++++ docs/testing.md | 6 +- 8 files changed, 229 insertions(+), 91 deletions(-) create mode 100644 crates/lumenwright-compat/Cargo.toml create mode 100644 crates/lumenwright-compat/src/lib.rs create mode 100644 crates/lumenwright-compat/tests/stated_sky.rs diff --git a/Cargo.lock b/Cargo.lock index 89e9737..8def3e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -844,7 +844,6 @@ dependencies = [ "image", "log", "lumenwright 0.2.0", - "lumenwright 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "rayon", "serde", "serde_json", @@ -853,6 +852,15 @@ dependencies = [ "zip", ] +[[package]] +name = "lumenwright-compat" +version = "0.2.0" +dependencies = [ + "lumenwright 0.2.0", + "lumenwright 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lumenwright-cli", +] + [[package]] name = "lumenwright-kernel" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 9c1cd44..53d2550 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["crates/lumenwright", "crates/lumenwright-cli", "crates/lumenwright-kernel"] +members = ["crates/lumenwright", "crates/lumenwright-cli", "crates/lumenwright-compat", "crates/lumenwright-kernel"] # The PTX crate only builds for nvptx64-nvidia-cuda; see scripts/build-ptx.sh. exclude = ["crates/lumenwright-ptx"] diff --git a/crates/lumenwright-cli/Cargo.toml b/crates/lumenwright-cli/Cargo.toml index 4ff97c5..6c2bd7c 100644 --- a/crates/lumenwright-cli/Cargo.toml +++ b/crates/lumenwright-cli/Cargo.toml @@ -44,7 +44,3 @@ default = ["gpu"] gpu = ["lumenwright/gpu"] # CUDA backend: needs the PTX from scripts/build-ptx.sh. cuda = ["lumenwright/cuda"] - -[dev-dependencies] -# 0.2.0 as published: the oracle a stated sky at no stated tick is held to. -lumenwright_020 = { package = "lumenwright", version = "=0.2.0", default-features = false } diff --git a/crates/lumenwright-cli/tests/library.rs b/crates/lumenwright-cli/tests/library.rs index 399752c..5512c9c 100644 --- a/crates/lumenwright-cli/tests/library.rs +++ b/crates/lumenwright-cli/tests/library.rs @@ -290,90 +290,6 @@ fn a_stated_sky_at_a_tick_is_the_vanilla_sky_at_it() { } } -/// A stated sky at no stated tick (`StatedSky::new`) draws what 0.2.0, which had no tick to -/// state, draws for the same request: byte for byte on the CPU in both modes, under a world -/// whose `DayTime` is midnight, where a tick's moon, stars or game look would show. -#[test] -fn a_stated_sky_at_no_tick_draws_as_0_2_0_did() { - let jar = require_jar!(); - let dir = tmp("stated-0-2-0"); - let world = dir.join("world"); - let mut fx = fixture::block_entity_gallery(); - fx.day_time = 18000; - fx.write(&world).unwrap(); - let quick = lumenwright::QuickSkyColours { - sky: Vec3::new(0.2, 0.3, 0.6), - fog: Vec3::new(0.7, 0.5, 0.4), - sun_dir: sky::sun_direction(10.0, 120.0), - sky_factor: 0.6, - }; - let hq = sky::hq_sky(quick.sun_dir, 1.4, 0.9); - let old_quick = lumenwright_020::QuickSkyColours { - sky: quick.sky, - fog: quick.fog, - sun_dir: quick.sun_dir, - sky_factor: quick.sky_factor, - }; - let old_hq = lumenwright_020::render::sky::HqSky { - sun_dir: hq.sun_dir, - sun_color: hq.sun_color, - sky_light: hq.sky_light, - }; - for mode in [Mode::Quick, Mode::Hq] { - let mut req = request(&world, &jar, mode); - req.sky = Sky::Stated(StatedSky::new(quick, hq)); - let cam = req.camera; - let old = lumenwright_020::Request { - world: lumenwright_020::WorldSource::Dir { - path: &world, - dimension: lumenwright_020::world::Dimension::Overworld, - }, - chunks: lumenwright_020::ChunkRect { - min: req.chunks.min, - max: req.chunks.max, - }, - jar: &jar, - unifont: req.unifont, - resource_packs: &[], - camera: lumenwright_020::scene::camera::Camera { - pos: cam.pos, - yaw: cam.yaw, - pitch: cam.pitch, - fov_y: cam.fov_y, - }, - mode: match mode { - Mode::Quick => lumenwright_020::render::Mode::Quick, - Mode::Hq => lumenwright_020::render::Mode::Hq, - }, - backend: lumenwright_020::render::BackendKind::Cpu, - width: req.width, - height: req.height, - entities: lumenwright_020::EntityPolicy::default(), - quick: lumenwright_020::QuickOptions { - ssaa: req.quick.ssaa, - draw_distance: req.quick.draw_distance, - ..lumenwright_020::QuickOptions::default() - }, - hq: lumenwright_020::HqOptions { - spp: req.hq.spp, - ..lumenwright_020::HqOptions::default() - }, - sky: lumenwright_020::Sky::Stated(lumenwright_020::StatedSky { - quick: old_quick, - hq: old_hq, - }), - }; - let want = lumenwright_020::render(&old).unwrap().rgba; - let got = lumenwright::render(&req).unwrap().rgba; - let differing = want - .chunks(4) - .zip(got.chunks(4)) - .filter(|(a, b)| a != b) - .count(); - assert_eq!(differing, 0, "{mode:?}: pixels differ from 0.2.0's"); - } -} - /// A JSON file from the client jar. fn jar_json(jar: &Path, name: &str) -> serde_json::Value { let f = std::fs::File::open(jar).unwrap(); diff --git a/crates/lumenwright-compat/Cargo.toml b/crates/lumenwright-compat/Cargo.toml new file mode 100644 index 0000000..42c71aa --- /dev/null +++ b/crates/lumenwright-compat/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "lumenwright-compat" +description = "Tests that hold the library to a published release's frames; never published" +edition.workspace = true +version.workspace = true +license.workspace = true +repository.workspace = true +rust-version = "1.90" +publish = false + +[dependencies] + +[dev-dependencies] +lumenwright = { path = "../lumenwright", default-features = false } +lumenwright-cli = { path = "../lumenwright-cli", default-features = false } +# 0.2.0 as published: the oracle a stated sky at no stated tick is held to. A published +# crate cannot depend on one crate version under two names, so the oracle lives here. +lumenwright_020 = { package = "lumenwright", version = "=0.2.0", default-features = false } diff --git a/crates/lumenwright-compat/src/lib.rs b/crates/lumenwright-compat/src/lib.rs new file mode 100644 index 0000000..1448973 --- /dev/null +++ b/crates/lumenwright-compat/src/lib.rs @@ -0,0 +1,4 @@ +//! Holds the `lumenwright` library to the frames a published release draws. The crate has +//! no code; its tests render one request through this tree and through the release as +//! published on crates.io and compare the bytes. It is never published: a published crate +//! cannot depend on one crate version under two names. diff --git a/crates/lumenwright-compat/tests/stated_sky.rs b/crates/lumenwright-compat/tests/stated_sky.rs new file mode 100644 index 0000000..24a4b90 --- /dev/null +++ b/crates/lumenwright-compat/tests/stated_sky.rs @@ -0,0 +1,192 @@ +//! A stated sky at no stated tick against 0.2.0 as published. Needs the vanilla client jar +//! (found as the command finds it, or via LUMENWRIGHT_MC_JAR) and skips without it. + +use std::path::PathBuf; + +use lumenwright::glam::{DVec3, Vec3}; +use lumenwright::render::sky; +use lumenwright::render::{BackendKind, Mode}; +use lumenwright::scene::camera::Camera; +use lumenwright::world::Dimension; +use lumenwright::{ + ChunkRect, EntityPolicy, HqOptions, QuickOptions, Request, Sky, StatedSky, WorldSource, +}; +use lumenwright_cli::{fixture, jar, unifont}; + +fn client_jar() -> Option { + if let Ok(p) = std::env::var("LUMENWRIGHT_MC_JAR") { + return Some(PathBuf::from(p)); + } + jar::resolve_jar(&jar::JarRequest { + explicit: None, + version: Some(fixture::VERSION_NAME), + allow_download: false, + }) + .ok() +} + +fn client_unifont() -> Option { + if let Ok(p) = std::env::var("LUMENWRIGHT_UNIFONT") { + return Some(PathBuf::from(p)); + } + unifont::resolve_unifont(&unifont::UnifontRequest { + explicit: None, + version: Some(fixture::VERSION_NAME), + allow_download: false, + }) + .ok() +} + +macro_rules! require_jar { + () => { + match client_jar() { + Some(j) => j, + None => { + eprintln!( + "skipping: no Minecraft {} client jar available", + fixture::VERSION_NAME + ); + return; + } + } + }; +} + +fn tmp(name: &str) -> PathBuf { + let d = std::env::temp_dir().join(format!("lumenwright-compat-{}-{name}", std::process::id())); + let _ = std::fs::remove_dir_all(&d); + std::fs::create_dir_all(&d).unwrap(); + d +} + +/// The block-entity gallery from the north side, as the library tests look at it. +const EYE: [f64; 3] = [9.5, 70.5, 11.5]; +const TARGET: [f64; 3] = [9.5, 65.4, -2.0]; +const RADIUS: i32 = 8; + +fn request<'a>( + world: &'a std::path::Path, + jar: &'a std::path::Path, + unifont: Option<&'a std::path::Path>, + mode: Mode, +) -> Request<'a> { + let pos = DVec3::from_array(EYE); + let (yaw, pitch) = Camera::look_at(pos, DVec3::from_array(TARGET)); + let camera = Camera { + pos, + yaw, + pitch, + fov_y: 60.0, + }; + Request { + world: WorldSource::Dir { + path: world, + dimension: Dimension::Overworld, + }, + chunks: ChunkRect::around(pos, RADIUS), + jar, + unifont, + resource_packs: &[], + camera, + mode, + backend: BackendKind::Cpu, + width: 192, + height: 108, + entities: EntityPolicy::default(), + quick: QuickOptions { + ssaa: 2, + draw_distance: (RADIUS * 16) as f32, + ..QuickOptions::default() + }, + hq: HqOptions { + spp: 8, + ..HqOptions::default() + }, + sky: Sky::default(), + } +} + +/// A stated sky at no stated tick (`StatedSky::new`) draws what 0.2.0, which had no tick to +/// state, draws for the same request: byte for byte on the CPU in both modes, under a world +/// whose `DayTime` is midnight, where a tick's moon, stars or game look would show. +#[test] +fn a_stated_sky_at_no_tick_draws_as_0_2_0_did() { + let jar = require_jar!(); + let unifont = client_unifont(); + let dir = tmp("stated-0-2-0"); + let world = dir.join("world"); + let mut fx = fixture::block_entity_gallery(); + fx.day_time = 18000; + fx.write(&world).unwrap(); + let quick = lumenwright::QuickSkyColours { + sky: Vec3::new(0.2, 0.3, 0.6), + fog: Vec3::new(0.7, 0.5, 0.4), + sun_dir: sky::sun_direction(10.0, 120.0), + sky_factor: 0.6, + }; + let hq = sky::hq_sky(quick.sun_dir, 1.4, 0.9); + let old_quick = lumenwright_020::QuickSkyColours { + sky: quick.sky, + fog: quick.fog, + sun_dir: quick.sun_dir, + sky_factor: quick.sky_factor, + }; + let old_hq = lumenwright_020::render::sky::HqSky { + sun_dir: hq.sun_dir, + sun_color: hq.sun_color, + sky_light: hq.sky_light, + }; + for mode in [Mode::Quick, Mode::Hq] { + let mut req = request(&world, &jar, unifont.as_deref(), mode); + req.sky = Sky::Stated(StatedSky::new(quick, hq)); + let cam = req.camera; + let old = lumenwright_020::Request { + world: lumenwright_020::WorldSource::Dir { + path: &world, + dimension: lumenwright_020::world::Dimension::Overworld, + }, + chunks: lumenwright_020::ChunkRect { + min: req.chunks.min, + max: req.chunks.max, + }, + jar: &jar, + unifont: req.unifont, + resource_packs: &[], + camera: lumenwright_020::scene::camera::Camera { + pos: cam.pos, + yaw: cam.yaw, + pitch: cam.pitch, + fov_y: cam.fov_y, + }, + mode: match mode { + Mode::Quick => lumenwright_020::render::Mode::Quick, + Mode::Hq => lumenwright_020::render::Mode::Hq, + }, + backend: lumenwright_020::render::BackendKind::Cpu, + width: req.width, + height: req.height, + entities: lumenwright_020::EntityPolicy::default(), + quick: lumenwright_020::QuickOptions { + ssaa: req.quick.ssaa, + draw_distance: req.quick.draw_distance, + ..lumenwright_020::QuickOptions::default() + }, + hq: lumenwright_020::HqOptions { + spp: req.hq.spp, + ..lumenwright_020::HqOptions::default() + }, + sky: lumenwright_020::Sky::Stated(lumenwright_020::StatedSky { + quick: old_quick, + hq: old_hq, + }), + }; + let want = lumenwright_020::render(&old).unwrap().rgba; + let got = lumenwright::render(&req).unwrap().rgba; + let differing = want + .chunks(4) + .zip(got.chunks(4)) + .filter(|(a, b)| a != b) + .count(); + assert_eq!(differing, 0, "{mode:?}: pixels differ from 0.2.0's"); + } +} diff --git a/docs/testing.md b/docs/testing.md index 7ad2e6a..6837d9e 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -26,9 +26,13 @@ cargo test --workspace --release parity in both modes (showcase, block-entity gallery and item gallery) and CPU-vs-CUDA parity in HQ mode, determinism, and the CLI. The library tests check that the CLI's PNG and the library's pixels are byte-equal in both modes, that a missing jar is refused by - name, that a stated sky is drawn as given, that a stated sky at a tick draws `Sky::Vanilla`'s frame at it byte for byte (quick, and HQ under the game's look and by time), that a stated sky at no tick draws 0.2.0's frame byte for byte (0.2.0 as published is a dev-dependency), that the vanilla sky's colour at noon, dusk and + name, that a stated sky is drawn as given, that a stated sky at a tick draws `Sky::Vanilla`'s frame at it byte for byte (quick, and HQ under the game's look and by time), that the vanilla sky's colour at noon, dusk and midnight matches the jar's day timeline within 2/255, and that CPU pixels are bit-identical on 1, 3 and 8 threads (showcase and item gallery). A far-plane test measures the high-frequency energy and the change under a sub-pixel camera move with and without mipmaps. Tests that need the client jar or a GPU skip with a message when it is missing. `LUMENWRIGHT_MC_JAR` points the tests at a jar. +* **Release tests** (in `lumenwright-compat`, a workspace member that is never published) + render one request through the tree and through a release as published on crates.io: a + stated sky at no tick draws 0.2.0's frame byte for byte in both modes. A published crate + cannot depend on one crate version under two names, so the oracle lives in its own crate.