Serve shader API 0.2 behind HORIZON_HDR_FRAME=float

With the switch, the game draws 0.2 packs ([frame] blend = "linear" is
still refused). A renderer made with a 0.2 pack picked draws a float frame
where the device can, else the 8-bit one; a pack picked or dropped later
runs on the frame there is, logged once. The renderer's material, sky,
weather, vertex and shadow entries decode a 0.2 stage's colours and encode
its results; scene_color, opaque_color and the water mirror are decoded
copies, shadow_color is float, and Frame's sky and fog colours are
decoded. The post chain ends in a window-size picture, and the pack's
[tonemap] (or Horizon's clip) draws it onto the frame, encoded at alpha 1.
With HDR on, HDR output lifts nothing of a 0.2 pack's world.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Nikita Maximilian GrimmandClaude Opus 5.5 committed 2026-11-05 15:37:14 +01:00
1 parent 36021e2559
commit 2bc92b6348
22 files changed
+1012 -203

No files matched your search

@@ -66,6 +66,7 @@ fn fixture(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
false,
false,
true,
false,
))?;
assert!(!renderer.pack_entity_shadows(), "no selected pack");
renderer.set_shader_pack(0, None, &[]).unwrap();
@@ -224,6 +224,7 @@ fn replay(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
settings.translucent_batching,
false,
false,
false,
))?;
drop(pixels);
render.pixels.pack();
@@ -126,6 +126,7 @@ fn fixture(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
false,
false,
true,
false,
))?;
renderer.resize(320, 180);
assert!(
+95 -26
View File
@@ -1375,9 +1375,12 @@ pub struct Renderer {
hdr_output: Option<hdr::Output>,
/// What every pipeline that draws the frame targets: the swapchain's 8-bit format, or
/// `Rgba16Unorm` when the renderer was made with HDR output on (`wide_frame`) and the
/// device draws and blends into it. A 16-bit frame with HDR off since reaches the
/// swapchain through `readback`.
/// device draws and blends into it, or `LINEAR_FRAME` (`frame_format`). A 16-bit frame
/// with HDR off since reaches the swapchain through `readback`.
frame_format: wgpu::TextureFormat,
/// Whether a pack picked or dropped since the renderer was made has been told it runs on
/// the frame that exists (`set_native_shader_pack`).
frame_kept_logged: bool,
/// The marker file of an HDR output that has not presented a frame yet
/// (`graphics::HDR_MARKER`).
hdr_marker: Option<std::path::PathBuf>,
@@ -1974,7 +1977,13 @@ fn draws_wide_frame(features: wgpu::Features, format: wgpu::TextureFormatFeature
features.contains(
wgpu::Features::TEXTURE_FORMAT_16BIT_NORM
| wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES,
) && format.allowed_usages.contains(
) && draws_frame(format)
}
/// Whether a format with the adapter's `format` features takes every usage and flag the
/// passes take of the frame.
fn draws_frame(format: wgpu::TextureFormatFeatures) -> bool {
format.allowed_usages.contains(
wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC
@@ -1984,8 +1993,14 @@ fn draws_wide_frame(features: wgpu::Features, format: wgpu::TextureFormatFeature
)
}
/// What a renderer's frame is drawn in: `WIDE_FRAME` when it is made with HDR on and Smooth
/// Gradients (`wide_frame`, `Graphics::wide_frame`), the surface offering HDR output
/// The frame of a renderer made with a shader API 0.2 pack picked under
/// `HORIZON_HDR_FRAME=float` (docs/design/hdr-linear-light.md): float, so values over 1
/// survive from the world to the tonemap.
const LINEAR_FRAME: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
/// What a renderer's frame is drawn in: `LINEAR_FRAME` for a 0.2 pack under the switch
/// (`linear`: `(picked, the device draws it)`); else `WIDE_FRAME` when it is made with HDR on
/// and Smooth Gradients (`wide_frame`, `Graphics::wide_frame`), the surface offering HDR output
/// (`hdr_offered`) and the device drawing it (`draws_wide_frame`): an 8-bit frame's steps band
/// in dark gradients, and HDR shows them. Else the swapchain's 8-bit format, as without HDR.
fn frame_format(
@@ -1993,8 +2008,11 @@ fn frame_format(
hdr_offered: bool,
device_draws_it: bool,
swapchain: wgpu::TextureFormat,
linear: (bool, bool),
) -> wgpu::TextureFormat {
if wide_frame && hdr_offered && device_draws_it {
if linear.0 && linear.1 {
LINEAR_FRAME
} else if wide_frame && hdr_offered && device_draws_it {
WIDE_FRAME
} else {
swapchain.remove_srgb_suffix()
@@ -2442,6 +2460,7 @@ impl Renderer {
translucent_batching: bool,
wide_frame: bool,
shader_pack: bool,
linear_pack: bool,
) -> Result<Self> {
let Opened {
instance,
@@ -2575,7 +2594,22 @@ impl Renderer {
adapter.get_texture_format_features(WIDE_FRAME),
);
crate::graphics::offer_smooth_gradients(wide_ok);
let frame_format = frame_format(wide_frame, hdr_output.is_some(), wide_ok, config.format);
let linear = (
linear_pack,
draws_frame(adapter.get_texture_format_features(LINEAR_FRAME)),
);
if linear == (true, false) {
log::warn!(
"Horizon renderer: the device can't draw a float frame; the shader API 0.2 pack runs on the 8-bit one"
);
}
let frame_format = frame_format(
wide_frame,
hdr_output.is_some(),
wide_ok,
config.format,
linear,
);
if previous.is_none() {
startup_step(format_args!("creating the pipelines"));
}
@@ -4709,6 +4743,7 @@ impl Renderer {
emission_stats,
hdr_output,
frame_format,
frame_kept_logged: false,
hdr_marker: None,
pack_post: None,
native: None,
@@ -6489,6 +6524,18 @@ impl Renderer {
block_light::LightTable,
)>,
) -> std::result::Result<Vec<String>, String> {
let linear = pack
.as_ref()
.is_some_and(|p| p.0.manifest.api == horizon_shader_api::Api::V0_2);
if (linear || self.frame_format == LINEAR_FRAME)
&& linear != (self.frame_format == LINEAR_FRAME)
&& !std::mem::replace(&mut self.frame_kept_logged, true)
{
log::info!(
"Horizon renderer: the {:?} frame stays until the renderer is made again; a shader API 0.2 pack picked or dropped since runs on it",
self.frame_format
);
}
let Some((report, constants, sprite_tags, sprite_materials, lights)) = pack else {
self.native = None;
return Ok(Vec::new());
@@ -9252,6 +9299,8 @@ impl Renderer {
}
}
if let Some(native) = &mut self.native {
let reflection =
reflection.map(|view| native.frame.linear_mirror(&self.device, &mut encoder, view));
native
.frame
.set_reflection(&self.device, reflection.as_ref());
@@ -9914,12 +9963,7 @@ impl Renderer {
depth.size(),
);
if let Some((color, _)) = &scaled {
let color = color.texture();
encoder.copy_texture_to_texture(
color.as_image_copy(),
native.frame.opaque_color.as_image_copy(),
color.size(),
);
native.frame.copy_opaque(&self.device, &mut encoder, color);
}
pass = world_pass_after_culling(
&mut encoder,
@@ -10551,12 +10595,18 @@ impl Renderer {
{
self.readback.present(&self.device, &mut encoder, image);
}
// 🤖 Agent: A 0.2 pack is shown at paper white: HDR output lifts nothing of its world.
let lifts_world = !frame.title
&& self
.native
.as_ref()
.is_none_or(|n| n.frame.api == horizon_shader_api::Api::V0_1);
match (&self.hdr, &surface_frame) {
(Some(hdr), Some(f)) => hdr.present(
&self.queue,
&mut encoder,
&f.texture,
!frame.title,
lifts_world,
self.own_timing.as_mut(),
),
// Offscreen nothing shows HDR: the target gets the frame as SDR shows it, for
@@ -10575,7 +10625,7 @@ impl Renderer {
.filter(|(request, _)| request.size.is_none())
.zip(self.hdr.as_ref())
.and_then(|(_, hdr)| {
let texture = hdr.capture(&self.device, &self.queue, &mut encoder, !frame.title)?;
let texture = hdr.capture(&self.device, &self.queue, &mut encoder, lifts_world)?;
let size = [self.config.width, self.config.height];
Some(copy_frame(&self.device, &mut encoder, &texture, size))
});
@@ -11994,20 +12044,39 @@ mod tests {
fn the_frame_is_16_bit_only_with_hdr_and_a_device_that_draws_it() {
use super::frame_format;
use wgpu::TextureFormat::{Bgra8Unorm, Bgra8UnormSrgb, Rgba8Unorm, Rgba16Unorm};
assert_eq!(frame_format(true, true, true, Bgra8Unorm), Rgba16Unorm);
for (wide_frame, hdr_offered, draws) in [
(false, true, true),
(true, false, true),
(true, true, false),
(false, false, false),
] {
// 🤖 Agent: No 0.2 pack (or a device without the float frame) leaves it as before.
for linear in [(false, false), (false, true), (true, false)] {
assert_eq!(
frame_format(wide_frame, hdr_offered, draws, Bgra8UnormSrgb),
Bgra8Unorm
frame_format(true, true, true, Bgra8Unorm, linear),
Rgba16Unorm
);
for (wide_frame, hdr_offered, draws) in [
(false, true, true),
(true, false, true),
(true, true, false),
(false, false, false),
] {
assert_eq!(
frame_format(wide_frame, hdr_offered, draws, Bgra8UnormSrgb, linear),
Bgra8Unorm
);
assert_eq!(
frame_format(wide_frame, hdr_offered, draws, Rgba8Unorm, linear),
Rgba8Unorm
);
}
}
}
/// A shader API 0.2 pack picked under `HORIZON_HDR_FRAME=float` gets the float frame
/// wherever the device draws it, HDR on or off.
#[test]
fn a_0_2_pack_gets_the_float_frame_where_the_device_draws_it() {
use super::{LINEAR_FRAME, frame_format};
use wgpu::TextureFormat::Bgra8UnormSrgb;
for (wide_frame, hdr_offered, draws) in [(true, true, true), (false, false, false)] {
assert_eq!(
frame_format(wide_frame, hdr_offered, draws, Rgba8Unorm),
Rgba8Unorm
frame_format(wide_frame, hdr_offered, draws, Bgra8UnormSrgb, (true, true)),
LINEAR_FRAME
);
}
}
@@ -162,11 +162,13 @@ fn horizon_material_color(in: HorizonMaterialOut, translucent: bool) -> vec4<f32
horizon_material_face_normal = normal;
horizon_surface = vec4<f32>(0.0);
let tangent = horizon_material_tangent(in.base, in.uv, normal);
return material(MaterialInput(albedo, in.tint, in.light, in.lightmap, in.position, in.uv,
translucent, horizon_sprite_tags(in.layer & 0x7fffu),
let out = material(MaterialInput(vec4(horizon_line_in(albedo.rgb), albedo.a),
vec4(horizon_line_in(in.tint.rgb), in.tint.a), in.light, horizon_line_in(in.lightmap),
in.position, in.uv, translucent, horizon_sprite_tags(in.layer & 0x7fffu),
horizon_sprite_material(in.layer & 0x7fffu), in.clip.xy / frame.resolution,
in.face, fluid, in.emission, normal, tangent,
horizon_terrain_native_colored_light(in.position), in.displaced)).color;
horizon_line_in(horizon_terrain_native_colored_light(in.position)), in.displaced)).color;
return vec4(horizon_line_out(out.rgb), out.a);
}
var<private> horizon_material_face_normal: vec3<f32>;
struct HorizonMaterialSolid {
@@ -332,7 +334,7 @@ fn horizon_material_displace(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: v
let fluid = high >= 8u && (high & 7u) < 6u;
let lit = min(vec2(f32(light.y), f32(light.x)) / 240.0, vec2(1.0));
return vertex(VertexInput(pos, uv, horizon_sprite_material(layer & 0x7fffu), lit,
vec4(color.rgb, 1.0), select(0u, high & 7u, fluid), u32(fluid)));
vec4(horizon_line_in(color.rgb), 1.0), select(0u, high & 7u, fluid), u32(fluid)));
}
";
@@ -349,12 +351,13 @@ fn shader_source(
source: &str,
tags: &[(u32, u32)],
materials: &[(u32, u32)],
vertex: bool,
displaced: bool,
(vertex, displaced): (bool, bool),
api: horizon_shader_api::Api,
) -> String {
format!(
"{source}\n// Horizon's terrain (terrain.wgsl)\n{}{}{}{}{ENTRIES}{}{}",
"{source}\n// Horizon's terrain (terrain.wgsl)\n{}{}{}{}{}{ENTRIES}{}{}",
vanilla_terrain(),
super::native_post::line_helpers(api),
if vertex {
DISPLACE_PACK
} else {
@@ -378,8 +381,9 @@ fn module(
materials: &[(u32, u32)],
vertex: bool,
displaced: bool,
api: horizon_shader_api::Api,
) -> Result<naga::Module, String> {
let text = shader_source(&stage.source, tags, materials, vertex, displaced);
let text = shader_source(&stage.source, tags, materials, (vertex, displaced), api);
naga::front::wgsl::parse_str(&text)
.map_err(|e| format!("{}: {}", stage.file, e.emit_to_string(&text)))
}
@@ -403,6 +407,7 @@ impl NativeMaterial {
shape.sprite_materials,
vertex,
displaced.is_some(),
frame.api,
)?;
let mut constants = present_constants(&module, constants);
let sprite_names = [
@@ -562,7 +567,7 @@ impl NativeMaterial {
#[cfg(test)]
mod tests {
use super::*;
use horizon_shader_api::{MANIFEST, Pack, StageKind, check};
use horizon_shader_api::{Api, MANIFEST, Pack, StageKind, check};
fn stage(material: &str) -> CheckedStage {
let manifest = "schema = 1\nid = \"t\"\nname = \"T\"\nversion = \"1\"\napi = \"0.1\"\n\
@@ -609,7 +614,7 @@ mod tests {
let tags = [(7, 1), (3, 1), (9, 2)];
let materials = [(5, 1000)];
assert!(layer_table("t", &tags).contains("case 3u, 7u: { return 1u; }"));
let module = module(&stage, &tags, &materials, false, false).unwrap();
let module = module(&stage, &tags, &materials, false, false, Api::V0_1).unwrap();
let info = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
@@ -622,8 +627,8 @@ mod tests {
&stage.source,
&tags,
&materials,
false,
false
(false, false),
Api::V0_1
))
)
});
@@ -647,6 +652,33 @@ mod tests {
);
}
/// The terrain shader of a 0.2 pack with a vertex stage, whose tint the renderer decodes.
#[test]
fn the_terrain_validates_with_a_0_2_pack() {
let manifest = "schema = 1\nid = \"t\"\nname = \"T\"\nversion = \"1\"\napi = \"0.2\"\n\
[material]\nfile = \"material.wgsl\"\n[vertex]\nfile = \"vertex.wgsl\"\n";
let material = "fn material(input: MaterialInput) -> Surface {\n \
return Surface(input.albedo * input.tint * vec4(input.colored_light, 1.0));\n}\n";
let vertex = "fn vertex(input: VertexInput) -> vec3<f32> {\n \
return input.position + input.tint.rgb;\n}\n";
let files = [
(MANIFEST, manifest),
("material.wgsl", material),
("vertex.wgsl", vertex),
]
.map(|(n, t)| (n.to_owned(), t.as_bytes().to_vec()))
.into();
let report = check(&Pack::from_files(files).unwrap()).unwrap();
let stage = report.stages.iter().find(|s| s.kind == StageKind::Material);
let module = module(stage.unwrap(), &[], &[], true, false, Api::V0_2).unwrap();
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
)
.validate(&module)
.unwrap();
}
#[test]
fn the_displaced_grid_validates_with_a_pack() {
let manifest = "schema = 1\nid = \"t\"\nname = \"T\"\nversion = \"1\"\napi = \"0.1\"\n\
@@ -661,14 +693,14 @@ mod tests {
let mut report = check(&Pack::from_files(files).unwrap()).unwrap();
let stage = report.stages.remove(0);
let materials = [(5, 1000)];
let module = module(&stage, &[], &materials, false, true).unwrap();
let module = module(&stage, &[], &materials, false, true, Api::V0_1).unwrap();
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
)
.validate(&module)
.unwrap_or_else(|e| {
let text = shader_source(&stage.source, &[], &materials, false, true);
let text = shader_source(&stage.source, &[], &materials, (false, true), Api::V0_1);
panic!("{}", e.emit_to_string(&text))
});
for entry in [
@@ -681,4 +713,116 @@ mod tests {
);
}
}
/// An identity material (vanilla's own lighting, `albedo * tint * lightmap`) under 0.2,
/// its colours decoded on the way in and encoded on the way out as the entries do, draws
/// what 0.1's does (vanilla's) within an 8-bit step.
#[test]
fn an_identity_0_2_material_is_vanilla_s_within_an_8_bit_step() {
let instance = crate::gfx::test_gpu::instance();
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
return;
};
let Ok((device, queue)) = pollster::block_on(adapter.request_device(&Default::default()))
else {
return;
};
// 🤖 Agent: The entries' call of `material`, with a 16×16 grid of colours.
let entry = "@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
return vec4<f32>(vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u)) * 2.0 - 1.0, 0.0, 1.0);
}
@fragment fn fs(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> {
let c = floor(p.xy) / 15.0;
let albedo = vec4(c.x, c.y, 1.0 - c.x, 1.0);
let tint = vec4(0.9, 1.0 - 0.5 * c.y, 0.7, 1.0);
let lightmap = vec3(c.y, 0.8, 1.0 - c.y);
let out = material(MaterialInput(vec4(horizon_line_in(albedo.rgb), albedo.a),
vec4(horizon_line_in(tint.rgb), tint.a), vec2(1.0), horizon_line_in(lightmap),
vec3(0.0), vec2(0.0), false, 0u, 0u, vec2(0.0), 0u, 0u, 0.0, vec3(0.0, 1.0, 0.0),
vec4(1.0, 0.0, 0.0, 1.0), horizon_line_in(vec3(0.0)), 0.0)).color;
return vec4(horizon_line_out(out.rgb), out.a);
}
";
let vanilla = "fn material(input: MaterialInput) -> Surface {\n \
return Surface(input.albedo * input.tint * vec4(input.lightmap, 1.0));\n}\n";
let format = wgpu::TextureFormat::Rgba8Unorm;
let draw = |api: Api| {
let manifest = format!(
"schema = 1\nid = \"t\"\nname = \"T\"\nversion = \"1\"\napi = \"{api}\"\n\
[material]\nfile = \"material.wgsl\"\n"
);
let files = [(MANIFEST, manifest.as_str()), ("material.wgsl", vanilla)]
.map(|(n, t)| (n.to_owned(), t.as_bytes().to_vec()))
.into();
let report = check(&Pack::from_files(files).unwrap()).unwrap();
let source = report.stages[0].source.clone()
+ horizon_shader_api::stages::PBR_STUB
+ super::super::native_post::line_helpers(api)
+ entry;
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(source.into()),
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: None,
vertex: wgpu::VertexState {
module: &module,
entry_point: Some("vs"),
compilation_options: Default::default(),
buffers: &[],
},
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some("fs"),
compilation_options: Default::default(),
targets: &[Some(format.into())],
}),
multiview_mask: None,
cache: None,
});
let size = wgpu::Extent3d {
width: 16,
height: 16,
depth_or_array_layers: 1,
};
let target = device.create_texture(&wgpu::TextureDescriptor {
label: None,
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = target.create_view(&Default::default());
let mut encoder = device.create_command_encoder(&Default::default());
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
depth_slice: None,
ops: Default::default(),
})],
..Default::default()
});
pass.set_pipeline(&pipeline);
pass.draw(0..3, 0..1);
drop(pass);
let screenshot = crate::gfx::copy_frame(&device, &mut encoder, &target, [16, 16]);
queue.submit([encoder.finish()]);
screenshot
.slice(..)
.map_async(wgpu::MapMode::Read, |r| r.unwrap());
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
screenshot.slice(..).get_mapped_range().unwrap().to_vec()
};
let (old, new) = (draw(Api::V0_1), draw(Api::V0_2));
let worst = old.iter().zip(&new).map(|(a, b)| a.abs_diff(*b)).max();
assert!(worst.is_some_and(|w| w <= 1), "{worst:?}");
}
}
@@ -101,7 +101,15 @@ impl NativePack {
let settings = chosen
.as_ref()
.map(|settings| super::native_shadow::fitted(device, settings));
let mut frame = NativeFrame::new(device, settings.as_ref(), textures, lights, terrain.pbr);
let api = report.manifest.api;
let mut frame = NativeFrame::new(
device,
settings.as_ref(),
textures,
lights,
terrain.pbr,
api,
);
// `[displacement]`, unless its option is off; nothing to displace without heights.
let displacement = report
.manifest
@@ -222,8 +230,13 @@ impl NativePack {
)
})
.transpose()?;
let post = (!post.is_empty())
.then(|| NativePost::new(device, post, &report.manifest, constants, format, &frame))
// 🤖 Agent: A 0.2 pack always has one: its decode and tonemap run without a chain too.
let tonemap = others.iter().find(|s| s.kind == StageKind::Tonemap);
let post = (!post.is_empty() || api == horizon_shader_api::Api::V0_2)
.then(|| {
let manifest = &report.manifest;
NativePost::new(device, post, manifest, constants, format, &frame, tonemap)
})
.transpose()?;
let mut labels: Vec<String> = sky.iter().map(|s| s.label.clone()).collect();
labels.extend(
@@ -7,11 +7,209 @@
//! (`NativeFrame`).
use glam::{Mat4, Vec3};
use horizon_shader_api::{Frame as FrameInputs, check::CheckedStage, stages::GROUP};
use horizon_shader_api::{
Api, Frame as FrameInputs, StageKind, check::CheckedStage, stages::GROUP,
};
use super::Frame;
const INTERMEDIATE: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
pub(super) const INTERMEDIATE: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
/// What the renderer's own entries call where a stage's colours cross into and out of it: as
/// they are under 0.1; under 0.2 vanilla's 2.2 power, as the prelude's `horizon_decode` and
/// `horizon_encode` (the shadow pass has no prelude, so these stand alone).
pub fn line_helpers(api: Api) -> &'static str {
match api {
Api::V0_1 => {
"fn horizon_line_in(c: vec3<f32>) -> vec3<f32> { return c; }
fn horizon_line_out(c: vec3<f32>) -> vec3<f32> { return c; }
"
}
Api::V0_2 => {
"fn horizon_line_in(c: vec3<f32>) -> vec3<f32> {
return pow(select(vec3<f32>(0.0), c, c > vec3<f32>(0.0)), vec3<f32>(2.2));
}
fn horizon_line_out(c: vec3<f32>) -> vec3<f32> {
return pow(select(vec3<f32>(0.0), c, c > vec3<f32>(0.0)), vec3<f32>(1.0 / 2.2));
}
"
}
}
}
/// 0.2's decoding copy: a picture in the frame's gamma into linear light in `INTERMEDIATE`, of
/// the same size, alpha 1 (`scene_color`, `opaque_color`, the mirror).
#[derive(Clone)]
pub struct Decoder(wgpu::RenderPipeline);
impl Decoder {
fn new(device: &wgpu::Device) -> Self {
let source = line_helpers(Api::V0_2).to_owned()
+ "@group(0) @binding(0) var horizon_from: texture_2d<f32>;
@vertex fn horizon_vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
return vec4<f32>(vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u)) * 2.0 - 1.0, 0.0, 1.0);
}
@fragment fn horizon_fs(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> {
return vec4<f32>(horizon_line_in(textureLoad(horizon_from, vec2<i32>(p.xy), 0).rgb), 1.0);
}
";
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Shader API 0.2 decode"),
source: wgpu::ShaderSource::Wgsl(source.into()),
});
let entries = (None, "horizon_vs", "horizon_fs");
Self(fullscreen_pipeline(
device,
"Shader API 0.2 decode",
&module,
entries,
INTERMEDIATE,
&[],
))
}
/// Draws `from` decoded onto `to`.
pub fn run(
&self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
from: &wgpu::TextureView,
to: &wgpu::TextureView,
) {
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Shader API 0.2 decode"),
layout: &self.0.get_bind_group_layout(0),
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(from),
}],
});
fullscreen_pass(
encoder,
"Shader API 0.2 decode",
to,
&self.0,
(0, &group),
None,
);
}
}
/// A full-screen pipeline `label` of `module`'s entries `(vertex, fragment)`, unblended.
fn fullscreen_pipeline(
device: &wgpu::Device,
label: &str,
module: &wgpu::ShaderModule,
(layout, vertex, fragment): (Option<&wgpu::PipelineLayout>, &str, &str),
format: wgpu::TextureFormat,
constants: &[(&str, f64)],
) -> wgpu::RenderPipeline {
let options = wgpu::PipelineCompilationOptions {
constants,
..Default::default()
};
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout,
vertex: wgpu::VertexState {
module,
entry_point: Some(vertex),
compilation_options: options.clone(),
buffers: &[],
},
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
fragment: Some(wgpu::FragmentState {
module,
entry_point: Some(fragment),
compilation_options: options,
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
})
}
/// One full-screen triangle of `pipeline` onto `view`, cleared first.
fn fullscreen_pass(
encoder: &mut wgpu::CommandEncoder,
label: &str,
view: &wgpu::TextureView,
pipeline: &wgpu::RenderPipeline,
(index, group): (u32, &wgpu::BindGroup),
timing: Option<&mut super::pack_timing::PassTiming>,
) {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(label),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
timestamp_writes: timing.and_then(|t| t.writes(label)),
..Default::default()
});
pass.set_pipeline(pipeline);
pass.set_bind_group(index, group, &[]);
pass.draw(0..3, 0..1);
}
/// The tonemap stage's entries as the renderer draws them: the check's, with the result
/// encoded into the frame's gamma, where the camera overlays and the HUD draw on it.
fn tonemap_entries() -> String {
StageKind::Tonemap.entries(Api::V0_2).replace(
"return vec4<f32>(min(horizon_sanitize(out), vec3<f32>(frame.headroom)), 1.0);",
"return vec4<f32>(horizon_encode(min(horizon_sanitize(out), vec3<f32>(frame.headroom))), 1.0);",
)
}
/// Horizon's tonemap for a 0.2 pack without `[tonemap]`: SDR's clip at white, which is the
/// entries' hold at `frame.headroom` (1 until 0.2 packs get HDR output).
const DEFAULT_TONEMAP: &str =
"fn tonemap(input: TonemapInput) -> vec3<f32> { return input.color; }\n";
/// A colour target of `INTERMEDIATE` and `size`, drawn and read.
fn intermediate(device: &wgpu::Device, label: &str, size: [u32; 2]) -> wgpu::TextureView {
device
.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width: size[0].max(1),
height: size[1].max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: INTERMEDIATE,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
})
.create_view(&Default::default())
}
/// A 0.2 pack's display (docs/api/shaders-reference-0.2.md): the world decoded into
/// `scene_color` (the first pass's `previous`), the chain ending in `picture`, and the tonemap
/// stage drawing that onto the screen.
struct Display {
decoder: Decoder,
/// The pack's `[tonemap]` label, `None` for Horizon's.
label: Option<String>,
tonemap: wgpu::RenderPipeline,
/// `scene_color` at the world's size; `picture` at the window's (none without a chain:
/// the tonemap reads `scene_color`).
scene: Option<([u32; 2], wgpu::TextureView)>,
picture: Option<([u32; 2], wgpu::TextureView)>,
}
/// The world's colour and depth views a set of bind groups was made for.
type WorldViews = (wgpu::TextureView, wgpu::TextureView, wgpu::TextureView);
@@ -98,6 +296,8 @@ pub struct NativePost {
named: Option<([u32; 2], Vec<wgpu::TextureView>)>,
/// The screen's colour format, the world target's too.
format: wgpu::TextureFormat,
/// Under 0.2: the decode, the picture and the tonemap.
display: Option<Display>,
}
/// The `frame` every stage of a pack reads (group 3, binding 0), written once a frame, and
@@ -145,6 +345,11 @@ pub struct NativeFrame {
frames: u32,
/// `frame.wetness`, and when it was last written.
wetness: (f32, Option<std::time::Instant>),
/// The shader API line of the pack, and under 0.2 the decoding copy and the mirror it
/// decoded (`linear_mirror`).
pub api: Api,
pub decoder: Option<Decoder>,
mirror: Option<wgpu::TextureView>,
}
/// A depth texture of `size` the world's depth is copied into.
@@ -179,7 +384,10 @@ fn copy_target(
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
// 🤖 Agent: RENDER_ATTACHMENT: 0.2 draws `opaque_color` decoded rather than copying it.
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
})
}
@@ -225,6 +433,7 @@ impl NativeFrame {
textures: Vec<(wgpu::TextureView, wgpu::Sampler)>,
lights: super::block_light::LightTable,
pbr: Option<&[wgpu::Texture; 2]>,
api: Api,
) -> Self {
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Native shader pack frame"),
@@ -290,7 +499,7 @@ impl NativeFrame {
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let shadow_map = super::native_shadow::ShadowMap::new(device, shadow);
let shadow_map = super::native_shadow::ShadowMap::new(device, shadow, api);
let block_light = super::block_light::BlockLight::new(device, lights);
let array_view = |texture: &wgpu::Texture| {
texture.create_view(&wgpu::TextureViewDescriptor {
@@ -347,6 +556,9 @@ impl NativeFrame {
shadow_map,
shadow_camera: shadow.map(super::native_shadow::camera),
reflects: false,
api,
decoder: (api == Api::V0_2).then(|| Decoder::new(device)),
mirror: None,
last: None,
eye: [f32::NAN; 4],
history_reset: true,
@@ -459,6 +671,7 @@ impl NativeFrame {
) {
let size = [depth.width(), depth.height()];
let (color_size, format) = match color {
Some(c) if self.decoder.is_some() => ([c.width(), c.height()], INTERMEDIATE),
Some(c) => ([c.width(), c.height()], c.format()),
None => ([1, 1], self.opaque_color.format()),
};
@@ -477,6 +690,46 @@ impl NativeFrame {
self.regroup(device);
}
/// Fills `opaque_color` from the world's colour `color`: a copy, under 0.2 decoded.
pub fn copy_opaque(
&self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
color: &wgpu::TextureView,
) {
match &self.decoder {
Some(d) => {
let to = self.opaque_color.create_view(&Default::default());
d.run(device, encoder, color, &to);
}
None => encoder.copy_texture_to_texture(
color.texture().as_image_copy(),
self.opaque_color.as_image_copy(),
color.texture().size(),
),
}
}
/// The water mirror `view` as the material reads it: under 0.2 decoded into a copy.
pub fn linear_mirror(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
view: wgpu::TextureView,
) -> wgpu::TextureView {
let Some(decoder) = &self.decoder else {
return view;
};
let size = [view.texture().width(), view.texture().height()];
let fits = |m: &wgpu::TextureView| [m.texture().width(), m.texture().height()] == size;
if !self.mirror.as_ref().is_some_and(fits) {
self.mirror = Some(intermediate(device, "Water mirror, linear", size));
}
let mirror = self.mirror.clone().unwrap();
decoder.run(device, encoder, &view, &mirror);
mirror
}
/// Sizes the surface target: the world's while a material stage writes it, else a blank.
pub fn fit_surface(&mut self, device: &wgpu::Device, size: [u32; 2]) {
if [self.surface.width(), self.surface.height()] != size {
@@ -532,6 +785,10 @@ impl NativeFrame {
self.shadow_camera.as_ref(),
);
inputs.wetness = self.wetness.0;
if self.api == Api::V0_2 {
inputs.sky_color = decoded(inputs.sky_color);
inputs.fog_color = decoded(inputs.fog_color);
}
if !self.reflects {
inputs.has_reflection = 0;
}
@@ -671,7 +928,8 @@ fn sampler_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
impl NativePost {
/// Pipelines for `stages` (the pack's post passes, in order) with the option values
/// `constants`, drawing onto a screen of `format`. Errors name the pass wgpu refused.
/// `constants`, drawing onto a screen of `format`; under 0.2 the last onto `picture`, and
/// `tonemap` (else Horizon's) onto the screen. Errors name the pass wgpu refused.
pub fn new(
device: &wgpu::Device,
stages: Vec<CheckedStage>,
@@ -679,6 +937,7 @@ impl NativePost {
constants: &[(String, f64)],
format: wgpu::TextureFormat,
frame: &NativeFrame,
tonemap: Option<&CheckedStage>,
) -> Result<Self, String> {
let targets = manifest.post_targets();
let outputs: Vec<Option<usize>> = manifest
@@ -751,7 +1010,7 @@ impl NativePost {
entry_point: Some("horizon_fs_post"),
compilation_options: options,
targets: &[Some(wgpu::ColorTargetState {
format: if plan[i].writes == Writes::Screen {
format: if plan[i].writes == Writes::Screen && frame.decoder.is_none() {
format
} else {
INTERMEDIATE
@@ -768,6 +1027,41 @@ impl NativePost {
}
passes.push((stage.label, pipeline));
}
let display = match &frame.decoder {
Some(decoder) => {
let source = tonemap.map_or_else(
|| StageKind::Tonemap.prelude(Api::V0_2) + DEFAULT_TONEMAP,
|s| s.source.clone(),
) + &tonemap_entries();
let file = tonemap.map_or("<tonemap>", |s| s.file.as_str());
let module = naga::front::wgsl::parse_str(&source)
.map_err(|e| format!("{file}: {}", e.emit_to_string(&source)))?;
let present = present_constants(&module, constants);
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Tonemap"),
source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module)),
});
let entries = (
Some(&pipeline_layout),
"horizon_vs_tonemap",
"horizon_fs_tonemap",
);
let pipeline =
fullscreen_pipeline(device, "Tonemap", &module, entries, format, &present);
if let Some(e) = pollster::block_on(scope.pop()) {
return Err(format!("{file}: {e}"));
}
Some(Display {
decoder: decoder.clone(),
label: tonemap.map(|s| s.label.clone()),
tonemap: pipeline,
scene: None,
picture: None,
})
}
None => None,
};
let sampler = |label, filter| {
device.create_sampler(&wgpu::SamplerDescriptor {
label: Some(label),
@@ -798,12 +1092,18 @@ impl NativePost {
named: None,
plan,
format,
display,
})
}
/// The passes' labels, in order.
/// The passes' labels, in order, then the pack's tonemap's.
pub fn labels(&self) -> Vec<&str> {
self.passes.iter().map(|(l, _)| l.as_str()).collect()
let tonemap = self.display.as_ref().and_then(|d| d.label.as_deref());
self.passes
.iter()
.map(|(l, _)| l.as_str())
.chain(tonemap)
.collect()
}
fn prepare(
@@ -811,8 +1111,26 @@ impl NativePost {
device: &wgpu::Device,
world: (&wgpu::TextureView, &wgpu::TextureView, &wgpu::TextureView),
size: [u32; 2],
window: [u32; 2],
history_reset: bool,
) {
if let Some(d) = &mut self.display {
if d.scene.as_ref().is_none_or(|s| s.0 != size) {
d.scene = Some((
size,
intermediate(device, "Shader API 0.2 scene_color", size),
));
self.groups = None;
}
let chain = !self.passes.is_empty();
if chain && d.picture.as_ref().is_none_or(|p| p.0 != window) {
d.picture = Some((
window,
intermediate(device, "Shader API 0.2 picture", window),
));
self.groups = None;
}
}
if !self.scales.is_empty() && self.named.as_ref().is_none_or(|n| n.0 != size) {
let views = self
.scales
@@ -900,85 +1218,98 @@ impl NativePost {
}
.create_view(&Default::default());
let blank = self.history_blank.create_view(&Default::default());
let groups = (0..self.passes.len())
// 🤖 Agent: Under 0.2 the passes read the world decoded.
let scene = self
.display
.as_ref()
.and_then(|d| d.scene.as_ref())
.map_or(world.0, |s| &s.1);
let group = |previous: &wgpu::TextureView, writes: Option<Writes>| {
let mut entries = vec![
wgpu::BindGroupEntry {
binding: 0,
resource: self.frame.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(scene),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(world.1),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(previous),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::Sampler(&self.linear),
},
wgpu::BindGroupEntry {
binding: 5,
resource: wgpu::BindingResource::Sampler(&self.nearest),
},
wgpu::BindGroupEntry {
binding: 6,
resource: wgpu::BindingResource::TextureView(&self.shadow.0),
},
wgpu::BindGroupEntry {
binding: 7,
resource: wgpu::BindingResource::Sampler(&self.shadow.1),
},
wgpu::BindGroupEntry {
binding: 8,
resource: wgpu::BindingResource::TextureView(&history),
},
wgpu::BindGroupEntry {
binding: 9,
resource: wgpu::BindingResource::TextureView(&self.shadow.2),
},
wgpu::BindGroupEntry {
binding: 11,
resource: wgpu::BindingResource::TextureView(world.2),
},
wgpu::BindGroupEntry {
binding: 10,
resource: wgpu::BindingResource::TextureView(&self.shadow.3),
},
];
entries.extend(pack_texture_group_entries(&self.textures));
// The pack's targets, a blank one in place of the one this pass writes.
if let Some((_, views)) = &self.named {
entries.extend(views.iter().enumerate().map(|(j, view)| {
let view = if writes == Some(Writes::Target(j)) {
&blank
} else {
view
};
wgpu::BindGroupEntry {
binding: horizon_shader_api::stages::POST_TARGET_BINDING + j as u32,
resource: wgpu::BindingResource::TextureView(view),
}
}));
}
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Native shader pack post"),
layout: &self.layout,
entries: &entries,
})
};
let mut groups: Vec<_> = (0..self.passes.len())
.map(|i| {
// The chain's picture so far: the world's before its first pass.
let previous = match (&self.targets, self.plan[i].reads) {
(Some((_, t)), Some(slot)) => &t[slot],
_ => world.0,
_ => scene,
};
let mut entries = vec![
wgpu::BindGroupEntry {
binding: 0,
resource: self.frame.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(world.0),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(world.1),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(previous),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::Sampler(&self.linear),
},
wgpu::BindGroupEntry {
binding: 5,
resource: wgpu::BindingResource::Sampler(&self.nearest),
},
wgpu::BindGroupEntry {
binding: 6,
resource: wgpu::BindingResource::TextureView(&self.shadow.0),
},
wgpu::BindGroupEntry {
binding: 7,
resource: wgpu::BindingResource::Sampler(&self.shadow.1),
},
wgpu::BindGroupEntry {
binding: 8,
resource: wgpu::BindingResource::TextureView(&history),
},
wgpu::BindGroupEntry {
binding: 9,
resource: wgpu::BindingResource::TextureView(&self.shadow.2),
},
wgpu::BindGroupEntry {
binding: 11,
resource: wgpu::BindingResource::TextureView(world.2),
},
wgpu::BindGroupEntry {
binding: 10,
resource: wgpu::BindingResource::TextureView(&self.shadow.3),
},
];
entries.extend(pack_texture_group_entries(&self.textures));
// The pack's targets, a blank one in place of the one this pass writes.
if let Some((_, views)) = &self.named {
entries.extend(views.iter().enumerate().map(|(j, view)| {
let view = if self.plan[i].writes == Writes::Target(j) {
&blank
} else {
view
};
wgpu::BindGroupEntry {
binding: horizon_shader_api::stages::POST_TARGET_BINDING + j as u32,
resource: wgpu::BindingResource::TextureView(view),
}
}));
}
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Native shader pack post"),
layout: &self.layout,
entries: &entries,
})
group(previous, Some(self.plan[i].writes))
})
.collect();
// 🤖 Agent: The tonemap's, last: the chain's picture, else the decoded world.
if let Some(d) = &self.display {
groups.push(group(d.picture.as_ref().map_or(scene, |p| &p.1), None));
}
self.groups = Some((
(world.0.clone(), world.1.clone(), world.2.clone()),
blank_history,
@@ -998,18 +1329,34 @@ impl NativePost {
history_reset: bool,
mut timing: Option<&mut super::pack_timing::PassTiming>,
) {
self.prepare(device, world, size, history_reset);
let window = [screen.texture().width(), screen.texture().height()];
self.prepare(device, world, size, window, history_reset);
if let Some(t) = timing.as_deref_mut() {
t.mark(device, encoder, (world.0, self.format));
}
let Some((_, _, groups)) = &self.groups else {
return;
};
if let Some(Display {
decoder,
scene: Some((_, scene)),
..
}) = &self.display
{
decoder.run(device, encoder, world.0, scene);
}
let last = match &self.display {
Some(Display {
picture: Some((_, picture)),
..
}) => picture,
_ => screen,
};
for (i, ((label, pipeline), group)) in self.passes.iter().zip(groups).enumerate() {
let view = match (self.plan[i].writes, &self.targets, &self.named) {
(Writes::Chain(slot), Some((_, t)), _) => &t[slot],
(Writes::Target(j), _, Some((_, n))) => &n[j],
_ => screen,
_ => last,
};
let attachments = [Some(wgpu::RenderPassColorAttachment {
view,
@@ -1030,9 +1377,13 @@ impl NativePost {
pass.set_bind_group(GROUP, group, &[]);
pass.draw(0..3, 0..1);
}
if let (Some(d), Some(group)) = (&self.display, groups.last()) {
let label = d.label.as_deref().unwrap_or("Tonemap");
fullscreen_pass(encoder, label, screen, &d.tonemap, (GROUP, group), timing);
}
// The chain's last picture becomes next frame's `history`, once every pass has read
// this frame's.
let before_last = &self.plan[..self.plan.len() - 1];
let before_last = &self.plan[..self.plan.len().saturating_sub(1)];
if let Some(Writes::Chain(slot)) = before_last
.iter()
.rfind(|p| matches!(p.writes, Writes::Chain(_)))
@@ -1067,6 +1418,11 @@ fn history_target(device: &wgpu::Device, label: &str, size: [u32; 2]) -> wgpu::T
})
}
/// Vanilla's gamma colour `c` in linear light (0.2's decode; negatives and NaN made 0).
fn decoded(c: [f32; 3]) -> [f32; 3] {
c.map(|v| if v > 0.0 { v.powf(2.2) } else { 0.0 })
}
/// `wetness` after `dt` seconds of `rain`: halfway there in 30 seconds while it rains
/// harder, in 10 while it dries (the half-lives of the `shaders/` format's `wetness`).
fn wetness(wetness: f32, rain: f32, dt: f32) -> f32 {
@@ -1363,7 +1719,14 @@ mod tests {
.into();
let report = check(&Pack::from_files(files).unwrap()).unwrap();
let format = wgpu::TextureFormat::Rgba8Unorm;
let frame = NativeFrame::new(&device, None, Vec::new(), Default::default(), None);
let frame = NativeFrame::new(
&device,
None,
Vec::new(),
Default::default(),
None,
Api::V0_1,
);
queue.write_buffer(
&frame.buffer,
0,
@@ -1376,6 +1739,7 @@ mod tests {
&[("gain".into(), 1.0)],
format,
&frame,
None,
)
.unwrap();
assert_eq!(post.labels(), ["a.wgsl", "b.wgsl"]);
@@ -1483,7 +1847,14 @@ file = \"b.wgsl\"
.into();
let report = check(&Pack::from_files(files).unwrap()).unwrap();
let format = wgpu::TextureFormat::Rgba8Unorm;
let frame = NativeFrame::new(&device, None, Vec::new(), Default::default(), None);
let frame = NativeFrame::new(
&device,
None,
Vec::new(),
Default::default(),
None,
Api::V0_1,
);
let mut post = NativePost::new(
&device,
report.stages,
@@ -1491,6 +1862,7 @@ file = \"b.wgsl\"
&[],
format,
&frame,
None,
)
.unwrap();
let usage = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING;
@@ -1595,7 +1967,14 @@ file = \"b.wgsl\"
.into();
let report = check(&Pack::from_files(files).unwrap()).unwrap();
let format = wgpu::TextureFormat::Rgba8Unorm;
let frame = NativeFrame::new(&device, None, Vec::new(), Default::default(), None);
let frame = NativeFrame::new(
&device,
None,
Vec::new(),
Default::default(),
None,
Api::V0_1,
);
let mut post = NativePost::new(
&device,
report.stages,
@@ -1603,6 +1982,7 @@ file = \"b.wgsl\"
&[],
format,
&frame,
None,
)
.unwrap();
let usage = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING;
@@ -1874,4 +2254,126 @@ file = \"b.wgsl\"
);
}
}
/// Shader API 0.2: the world decoded, through a chain that passes it on or none, and
/// Horizon's tonemap or the pack's identity one, comes back encoded as it went in (within
/// an 8-bit step), at alpha 1.
#[test]
fn a_0_2_world_comes_back_through_decode_tonemap_and_encode() {
assert!(tonemap_entries().contains("horizon_encode(min(horizon_sanitize(out)"));
let Some((device, queue)) = device() else {
return;
};
let post = "[[post]]\nfile = \"a.wgsl\"\n[[post]]\nfile = \"b.wgsl\"\n";
let curve = "[tonemap]\nfile = \"t.wgsl\"\n";
// 🤖 Agent: A pack needs a stage: the first has only a sky, and no chain or tonemap.
let sky = "[sky]\nfile = \"s.wgsl\"\n";
for extra in [
sky.into(),
post.into(),
curve.into(),
format!("{post}{curve}"),
] {
let manifest = format!(
"schema = 1\nid = \"t\"\nname = \"T\"\nversion = \"1\"\napi = \"0.2\"\n{extra}"
);
let pass = "fn post(input: PostInput) -> vec4<f32> { return input.color; }\n";
let identity = "fn tonemap(input: TonemapInput) -> vec3<f32> { return input.color; }\n";
let mut files = vec![(MANIFEST, manifest.as_str())];
if extra.contains("[[post]]") {
files.extend([("a.wgsl", pass), ("b.wgsl", pass)]);
}
if extra.contains("[tonemap]") {
files.push(("t.wgsl", identity));
}
if extra.contains("[sky]") {
files.push((
"s.wgsl",
"fn sky(input: SkyInput) -> vec4<f32> { return input.color; }\n",
));
}
let files = files
.into_iter()
.map(|(n, t)| (n.to_owned(), t.as_bytes().to_vec()))
.collect();
let report = check(&Pack::from_files(files).unwrap()).unwrap();
let (passes, others): (Vec<_>, Vec<_>) = report
.stages
.into_iter()
.partition(|s| s.kind == StageKind::Post);
let format = wgpu::TextureFormat::Rgba8Unorm;
let frame = NativeFrame::new(
&device,
None,
Vec::new(),
Default::default(),
None,
Api::V0_2,
);
let inputs = FrameInputs {
headroom: 1.0,
..Default::default()
};
queue.write_buffer(&frame.buffer, 0, bytemuck::bytes_of(&inputs));
let tonemap = others.iter().find(|s| s.kind == StageKind::Tonemap);
let mut post = NativePost::new(
&device,
passes,
&report.manifest,
&[],
format,
&frame,
tonemap,
)
.unwrap();
assert_eq!(post.labels().last() == Some(&"t.wgsl"), tonemap.is_some());
let usage =
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING;
let color = texture(&device, format, usage).create_view(&Default::default());
let depth = texture(&device, wgpu::TextureFormat::Depth32Float, usage)
.create_view(&Default::default());
let screen = texture(
&device,
format,
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
);
let mut encoder = device.create_command_encoder(&Default::default());
let clear = wgpu::Color {
r: 0.8,
g: 0.4,
b: 0.2,
a: 0.5,
};
drop(encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &color,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(clear),
store: wgpu::StoreOp::Store,
},
})],
..Default::default()
}));
let view = screen.create_view(&Default::default());
let world = (&color, &depth, &frame.surface_view);
post.run(&device, &mut encoder, world, &view, [4, 4], true, None);
let buffer = crate::gfx::copy_frame(&device, &mut encoder, &screen, [4, 4]);
queue.submit([encoder.finish()]);
buffer
.slice(..)
.map_async(wgpu::MapMode::Read, |r| r.unwrap());
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
let pixel = buffer.slice(..).get_mapped_range().unwrap()[..4].to_vec();
let want = [204, 102, 51, 255];
assert!(
pixel
.iter()
.zip(want)
.all(|(&got, want)| got.abs_diff(want) <= 1),
"{extra}: {pixel:?}"
);
}
}
}
@@ -6,7 +6,7 @@
//! draw lists (`ShadowView`) serve both; the map's texels are distorted toward the camera as the
//! prelude's `shadow_position` undoes.
use horizon_shader_api::{Frame as FrameInputs, manifest::Shadow as Settings, stages::GROUP};
use horizon_shader_api::{Api, Frame as FrameInputs, manifest::Shadow as Settings, stages::GROUP};
use horizon_shaders::Shadow;
use super::native_material::TerrainShape;
@@ -75,7 +75,7 @@ fn horizon_shadow_cutout(in: HorizonShadowOut) {
@fragment fn horizon_shadow_fs_translucent(in: HorizonShadowOut) -> @location(0) vec4<f32> {
let color = horizon_terrain_sprite(in.uv, in.layer & 0x7fffu);
if color.a < 0.01 { discard; }
return vec4(color.rgb * in.tint, select(min(color.a, 0.99), 1.0, in.fluid != 0u));
return vec4(horizon_line_in(color.rgb * in.tint), select(min(color.a, 0.99), 1.0, in.fluid != 0u));
}
";
@@ -152,8 +152,13 @@ pub struct ShadowGrid<'a> {
pub normals: &'a wgpu::TextureView,
}
/// `shadow_color`'s format.
const SHADOW_COLOR: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
/// `shadow_color`'s format: under 0.2 it holds linear light, which 8 bits would band.
fn shadow_color(api: Api) -> wgpu::TextureFormat {
match api {
Api::V0_1 => wgpu::TextureFormat::Rgba8Unorm,
Api::V0_2 => super::native_post::INTERMEDIATE,
}
}
/// The shader pack's shadow camera for `settings`: orthographic over `distance` each side,
/// staying put while the camera walks a `SNAP`-block cell. The map is distorted about its
@@ -218,7 +223,7 @@ pub struct ShadowMap {
}
impl ShadowMap {
pub fn new(device: &wgpu::Device, settings: Option<&Settings>) -> Self {
pub fn new(device: &wgpu::Device, settings: Option<&Settings>, api: Api) -> Self {
let size = settings.map_or(1, |s| s.resolution);
let texture = |label, format, usage| {
device.create_texture(&wgpu::TextureDescriptor {
@@ -250,7 +255,7 @@ impl ShadowMap {
);
let color = texture(
"Native shader pack shadow colour",
SHADOW_COLOR,
shadow_color(api),
wgpu::TextureUsages::empty(),
);
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
@@ -474,6 +479,7 @@ fn shader_source(
vertex: Option<&str>,
materials: &[(u32, u32)],
grid: Option<&str>,
api: Api,
) -> String {
let table = super::native_material::layer_table("horizon_sprite_material", materials);
let (head, displace) = match (grid, vertex) {
@@ -504,11 +510,12 @@ fn shader_source(
),
};
format!(
"{head}// Horizon's terrain (terrain.wgsl)\n{}{solid}{displace}{ENTRIES}",
"{head}// Horizon's terrain (terrain.wgsl)\n{}{solid}{}{displace}{ENTRIES}",
prefixed(
section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)"),
PREFIX
)
),
super::native_post::line_helpers(api)
)
}
@@ -527,6 +534,7 @@ impl NativeShadow {
vertex.map(|s| s.source.as_str()),
shape.sprite_materials,
grid.as_ref().map(|g| g.material.source.as_str()),
frame.api,
);
let module = naga::front::wgsl::parse_str(&text)
.map_err(|e| format!("shadow pass: {}", e.emit_to_string(&text)))?;
@@ -613,7 +621,7 @@ impl NativeShadow {
attributes: &instance_attributes,
};
let color_target = [Some(wgpu::ColorTargetState {
format: SHADOW_COLOR,
format: shadow_color(frame.api),
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})];
@@ -731,8 +739,9 @@ mod tests {
#[test]
fn the_shadow_shader_validates() {
for materials in [&[][..], &[(3, 7), (4, 8), (9, 7)][..]] {
let text = shader_source(&shadow_solid(materials, &[7]), None, materials, None);
let lines = [Api::V0_1, Api::V0_2];
for (materials, api) in [&[][..], &[(3, 7), (4, 8), (9, 7)][..]].iter().zip(lines) {
let text = shader_source(&shadow_solid(materials, &[7]), None, materials, None, api);
let module = naga::front::wgsl::parse_str(&text).unwrap();
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
@@ -767,6 +776,7 @@ mod tests {
None,
&materials,
Some(&stage.source),
Api::V0_1,
);
let module = naga::front::wgsl::parse_str(&text)
.unwrap_or_else(|e| panic!("{}", e.emit_to_string(&text)));
@@ -17,7 +17,8 @@ const PREFIX: &str = "horizon_sky_";
const PACK_ENTRY: &str = "
@fragment fn horizon_sky_fs_pack(in: horizon_sky_SkyOut) -> @location(0) vec4<f32> {
let d = horizon_sky_view_ray(in.ndc);
return vec4(sky(SkyInput(d, vec4(horizon_sky_sky_along_ray(d), 1.0))).rgb, 1.0);
let color = vec4(horizon_line_in(horizon_sky_sky_along_ray(d)), 1.0);
return vec4(horizon_line_out(sky(SkyInput(d, color)).rgb), 1.0);
}
";
@@ -38,10 +39,11 @@ fn vanilla_sky() -> String {
}
/// The sky shader for the pack's sky stage `source`.
fn shader_source(source: &str) -> String {
fn shader_source(source: &str, api: horizon_shader_api::Api) -> String {
format!(
"{source}\n// Horizon's sky (terrain.wgsl)\n{}{PACK_ENTRY}",
vanilla_sky()
"{source}\n// Horizon's sky (terrain.wgsl)\n{}{}{PACK_ENTRY}",
vanilla_sky(),
super::native_post::line_helpers(api)
)
}
@@ -99,7 +101,7 @@ fn build(
frame: &NativeFrame,
stage: &CheckedStage,
) -> Result<(naga::Module, wgpu::PipelineLayout), String> {
let text = shader_source(&stage.source);
let text = shader_source(&stage.source, frame.api);
let module = naga::front::wgsl::parse_str(&text)
.map_err(|e| format!("{}: {}", stage.file, e.emit_to_string(&text)))?;
let mut groups: [Option<&wgpu::BindGroupLayout>; GROUP as usize + 1] = Default::default();
@@ -195,7 +197,7 @@ mod tests {
// A pack may use the renderer's names for its own things.
let own = "override gain: f32;\nconst PI: f32 = 3.0;\nfn view_ray() -> f32 { return PI; }\n\
fn sky(input: SkyInput) -> vec4<f32> { return input.color * gain * view_ray(); }\n";
let text = shader_source(&stage(own).source);
let text = shader_source(&stage(own).source, horizon_shader_api::Api::V0_1);
let module = naga::front::wgsl::parse_str(&text).unwrap_or_else(|e| {
panic!("{}", e.emit_to_string(&text));
});
@@ -252,7 +254,14 @@ mod tests {
resource: globals_buffer.as_entire_binding(),
}],
});
let frame = NativeFrame::new(&device, None, Vec::new(), Default::default(), None);
let frame = NativeFrame::new(
&device,
None,
Vec::new(),
Default::default(),
None,
horizon_shader_api::Api::V0_1,
);
queue.write_buffer(
&frame.buffer,
0,
@@ -323,4 +323,26 @@ mod tests {
let unorm = wgpu::TextureFormat::Rgba16Unorm;
reaches_an_8_bit_swapchain(&device, &queue, unorm, color, [0, 51, 255, 255]);
}
/// `Rgba16Float`, the frame of a shader API 0.2 pack under `HORIZON_HDR_FRAME=float`
/// (`Renderer::frame_format`): what the tonemap left over 1 is clipped on the way.
#[test]
fn a_float_frame_reaches_an_8_bit_swapchain() {
let instance = crate::gfx::test_gpu::instance();
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
return;
};
let Ok((device, queue)) = pollster::block_on(adapter.request_device(&Default::default()))
else {
return;
};
let color = wgpu::Color {
r: 1.5,
g: 0.2,
b: -0.5,
a: 1.0,
};
let float = wgpu::TextureFormat::Rgba16Float;
reaches_an_8_bit_swapchain(&device, &queue, float, color, [0, 51, 255, 255]);
}
}
+23 -17
View File
@@ -217,15 +217,19 @@ struct HorizonWeatherOut {
textureDimensions(horizon_entity_atlas));
let color = textureLoad(horizon_entity_atlas, texel, 0) * in.color;
if color.a < 0.1 { discard; }
return weather(WeatherInput(color, in.light, in.position, in.snow != 0u));
let input = WeatherInput(vec4(horizon_line_in(color.rgb), color.a), in.light, in.position,
in.snow != 0u);
let out = weather(input);
return vec4(horizon_line_out(out.rgb), out.a);
}
";
/// The shader of a native pack's weather stage `source`.
fn native_source(source: &str) -> String {
fn native_source(source: &str, api: horizon_shader_api::Api) -> String {
format!(
"{source}\n// Horizon's weather (entity.wgsl)\n{}{NATIVE_ENTRIES}",
super::wgsl_prefix::prefixed(&super::entity_shader(), "horizon_entity_")
"{source}\n// Horizon's weather (entity.wgsl)\n{}{}{NATIVE_ENTRIES}",
super::wgsl_prefix::prefixed(&super::entity_shader(), "horizon_entity_"),
super::native_post::line_helpers(api)
)
}
@@ -242,7 +246,7 @@ pub(super) fn native_pipeline(
constants: &[(String, f64)],
) -> Result<wgpu::RenderPipeline, String> {
use horizon_shader_api::stages::GROUP;
let text = native_source(&stage.source);
let text = native_source(&stage.source, frame.api);
let module = naga::front::wgsl::parse_str(&text)
.map_err(|e| format!("{}: {}", stage.file, e.emit_to_string(&text)))?;
let present = super::native_post::present_constants(&module, constants);
@@ -318,19 +322,21 @@ mod tests {
#[test]
fn a_native_weather_stage_validates() {
use horizon_shader_api::stages::StageKind;
let source = StageKind::Weather.prelude(horizon_shader_api::Api::V0_1)
+ "fn weather(input: WeatherInput) -> vec4<f32> {\n \
return input.color * vec4(input.light, input.position.x, f32(input.snow));\n}\n";
let text = native_source(&source);
let module = naga::front::wgsl::parse_str(&text)
use horizon_shader_api::{Api, stages::StageKind};
for api in [Api::V0_1, Api::V0_2] {
let source = StageKind::Weather.prelude(api)
+ "fn weather(input: WeatherInput) -> vec4<f32> {\n \
return input.color * vec4(input.light, input.position.x, f32(input.snow));\n}\n";
let text = native_source(&source, api);
let module = naga::front::wgsl::parse_str(&text)
.unwrap_or_else(|e| panic!("{}", e.emit_to_string(&text)));
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
)
.validate(&module)
.unwrap_or_else(|e| panic!("{}", e.emit_to_string(&text)));
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
)
.validate(&module)
.unwrap_or_else(|e| panic!("{}", e.emit_to_string(&text)));
}
}
use crate::weather::{Parameters, Time};
use wgpu::util::DeviceExt;
+7 -5
View File
@@ -14691,7 +14691,7 @@ impl Game<'_> {
self.settings.horizon.api,
self.settings.horizon.translucent_batching,
self.settings.horizon.wide_frame(),
shader_packs::picked_pack_draws_flat(&self.settings),
shader_packs::picked_pack_shape(&self.settings),
(
self.settings.file.int("textureFiltering"),
self.settings.file.int("maxAnisotropyBit"),
@@ -14732,7 +14732,7 @@ impl Game<'_> {
self.settings.horizon.api,
self.settings.horizon.translucent_batching,
self.settings.horizon.wide_frame(),
shader_packs::picked_pack_draws_flat(&self.settings),
shader_packs::picked_pack_shape(&self.settings),
(
self.settings.file.int("textureFiltering"),
self.settings.file.int("maxAnisotropyBit"),
@@ -21138,7 +21138,7 @@ fn build_renderer(
// HDR with Smooth Gradients (`Graphics::wide_frame`): a 16-bit frame where HDR output is
// offered (`Renderer::new`).
wide_frame: bool,
shader_pack: bool,
shader_pack: shader_packs::PickedPack,
filtering: (i32, i32),
) -> Result<Renderer> {
let assets = &pack.render;
@@ -21177,7 +21177,8 @@ fn build_renderer(
api,
translucent_batching,
wide_frame,
shader_pack,
shader_pack.draws_flat,
shader_pack.linear,
))?;
renderer.hud.sprite_animations = atlas
.sprite_animations
@@ -21249,6 +21250,7 @@ fn loading_renderer(
// An 8-bit frame, which HDR output takes as well.
false,
false,
false,
))?;
if let Some(atlas) = atlas {
renderer.hud.sprite_animations = atlas
@@ -21840,7 +21842,7 @@ impl Loading {
self.settings.horizon.api,
self.settings.horizon.translucent_batching,
self.settings.horizon.wide_frame(),
shader_packs::picked_pack_draws_flat(&self.settings),
shader_packs::picked_pack_shape(&self.settings),
(
self.settings.file.int("textureFiltering"),
self.settings.file.int("maxAnisotropyBit"),
@@ -56,7 +56,7 @@ impl Game<'_> {
return;
};
let result = Pack::read(path).map_err(|e| vec![e]).and_then(|pack| {
if let Some(why) = not_drawn(&pack.manifest) {
if let Some(why) = not_drawn(&pack.manifest, linear_frame_switch()) {
return Err(vec![why]);
}
let report = check(&pack)
@@ -141,22 +141,29 @@ impl Game<'_> {
}
/// Why the game doesn't draw a pack the check may still pass: what it asks of shader API 0.2
/// (docs/design/hdr-linear-light.md, steps 3 and 4) isn't served yet.
fn not_drawn(m: &Manifest) -> Option<String> {
/// (docs/design/hdr-linear-light.md, steps 3 and 4) isn't served yet. 0.2 itself is drawn
/// under `HORIZON_HDR_FRAME=float` (`switch`).
fn not_drawn(m: &Manifest, switch: bool) -> Option<String> {
if m.blend == Blend::Linear {
return Some(
"[frame] blend = \"linear\" isn't served yet: leave it out, or set \"gamma\"".into(),
);
}
(m.api != Api::V0_1).then(|| {
(m.api != Api::V0_1 && !switch).then(|| {
format!(
"it is written for shader API {}, which is experimental and not drawn yet: \
horizon-shader check takes it, the game takes shader API 0.1",
"it is written for shader API {}, which is experimental and drawn only under \
HORIZON_HDR_FRAME=float: horizon-shader check takes it, the game takes shader API 0.1",
m.api
)
})
}
/// `HORIZON_HDR_FRAME=float`: shader API 0.2 packs are drawn, on a float frame where the
/// renderer is made with one picked.
pub(super) fn linear_frame_switch() -> bool {
std::env::var("HORIZON_HDR_FRAME").is_ok_and(|v| v == "float")
}
/// A hash of the names, sizes and modification times of the pack's files and of its saved
/// option values (`None` when the pack is unreadable).
fn fingerprint(path: &Path) -> Option<u64> {
@@ -201,14 +208,17 @@ mod tests {
}
#[test]
fn only_shader_api_0_1_is_drawn() {
assert_eq!(not_drawn(&manifest("0.1", "")), None);
let why = not_drawn(&manifest("0.2", "")).unwrap();
fn shader_api_0_2_is_drawn_only_under_the_switch() {
for switch in [false, true] {
assert_eq!(not_drawn(&manifest("0.1", ""), switch), None);
let why = not_drawn(&manifest("0.2", "[frame]\nblend = \"linear\"\n"), switch);
assert!(why.unwrap().contains("blend = \"linear\" isn't served yet"));
}
let why = not_drawn(&manifest("0.2", ""), false).unwrap();
assert!(
why.contains("shader API 0.2, which is experimental"),
why.contains("drawn only under HORIZON_HDR_FRAME=float"),
"{why}"
);
let why = not_drawn(&manifest("0.2", "[frame]\nblend = \"linear\"\n")).unwrap();
assert!(why.contains("blend = \"linear\" isn't served yet"), "{why}");
assert_eq!(not_drawn(&manifest("0.2", ""), true), None);
}
}
+1 -1
View File
@@ -356,7 +356,7 @@ impl Game<'_> {
self.settings.horizon.api,
self.settings.horizon.translucent_batching,
self.settings.horizon.wide_frame(),
super::shader_packs::picked_pack_draws_flat(&self.settings),
super::shader_packs::picked_pack_shape(&self.settings),
(
self.settings.file.int("textureFiltering"),
self.settings.file.int("maxAnisotropyBit"),
+21 -5
View File
@@ -79,12 +79,28 @@ fn picked_pack(settings: &horizon_client::settings::Settings) -> &str {
)
}
/// Whether the pack picked (`picked_pack`) draws flat where the device can (`pack_draws_flat`):
/// a renderer made now lays the sections out for it before the first world frame loads it
/// (`Renderer::new`).
pub(super) fn picked_pack_draws_flat(settings: &horizon_client::settings::Settings) -> bool {
/// What a renderer made now readies for the pack picked (`picked_pack`) before the first world
/// frame loads it (`Renderer::new`).
#[derive(Clone, Copy)]
pub(crate) struct PickedPack {
/// It draws flat where the device can (`pack_draws_flat`): the sections are laid out so.
pub draws_flat: bool,
/// It is a shader API 0.2 pack under `HORIZON_HDR_FRAME=float`: the frame is float.
pub linear: bool,
}
pub(super) fn picked_pack_shape(settings: &horizon_client::settings::Settings) -> PickedPack {
let picked = picked_pack(settings);
!picked.is_empty() && pack_draws_flat(&Game::shader_packs_dir().join(picked))
let path = Game::shader_packs_dir().join(picked);
let linear = || {
super::native_shaders::linear_frame_switch()
&& horizon_shader_api::Pack::read(&path)
.is_ok_and(|p| p.manifest.api == horizon_shader_api::Api::V0_2)
};
PickedPack {
draws_flat: !picked.is_empty() && pack_draws_flat(&path),
linear: !picked.is_empty() && linear(),
}
}
/// Whether the pack at `path` has passes that draw the terrain flat where the device can,
+2 -2
View File
@@ -363,7 +363,7 @@ impl<'a> Split<'a> {
old.release_surface();
let api = self.host.settings.horizon.api;
let batching = self.host.settings.horizon.translucent_batching;
let shader_pack = super::shader_packs::picked_pack_draws_flat(&self.host.settings);
let shader_pack = super::shader_packs::picked_pack_shape(&self.host.settings);
let built = build_renderer(
window.clone(),
true,
@@ -447,7 +447,7 @@ impl<'a> Split<'a> {
old.take_back(main.compositor);
let api = self.host.settings.horizon.api;
let batching = self.host.settings.horizon.translucent_batching;
let shader_pack = super::shader_packs::picked_pack_draws_flat(&self.host.settings);
let shader_pack = super::shader_packs::picked_pack_shape(&self.host.settings);
let built = build_renderer(
window.clone(),
false,
@@ -482,7 +482,7 @@ impl Split<'_> {
host.settings.horizon.translucent_batching,
host.settings.horizon.wide_frame(),
// The pack its own game loads, from its profile.
crate::game::shader_packs::picked_pack_draws_flat(&guest.settings),
crate::game::shader_packs::picked_pack_shape(&guest.settings),
(
guest.settings.file.int("textureFiltering"),
guest.settings.file.int("maxAnisotropyBit"),
@@ -2,5 +2,5 @@
[Golden Hour](../golden_hour) written for shader API 0.2 ([shaders-reference-0.2.md](../../../../docs/api/shaders-reference-0.2.md)):
the same sky and post passes in linear light, and a `[tonemap]` stage of its own. 0.2 is experimental, so this is a
test pack of the SDK: the game refuses it and the registry doesn't list it. Check it with
`horizon-shader check crates/horizon-shader-api/packs/golden_hour_0_2`.
test pack of the SDK: the game draws it only with `HORIZON_HDR_FRAME=float` set, and the registry doesn't list it.
Check it with `horizon-shader check crates/horizon-shader-api/packs/golden_hour_0_2`.
+3 -3
View File
@@ -39,9 +39,9 @@ pub fn path(api: Api) -> &'static str {
/// 0.2's rules, after the introduction.
const RULES_0_2: &str = "\
**Experimental** ([stability.md](stability.md)): `horizon-shader check` takes 0.2 packs, but the game refuses them
for now and the registry doesn't list them, and anything here may change in any release. The design:
[hdr-linear-light.md](../design/hdr-linear-light.md).
**Experimental** ([stability.md](stability.md)): `horizon-shader check` takes 0.2 packs, the game draws them only
with `HORIZON_HDR_FRAME=float` set, the registry doesn't list them, and anything here may change in any release.
The design: [hdr-linear-light.md](../design/hdr-linear-light.md).
## Colour
+3 -3
View File
@@ -3,9 +3,9 @@
Generated from `crates/horizon-shader-api` (`HORIZON_WRITE_DOCS=1 cargo test -p horizon-shader-api --test reference`
rewrites it; the test fails when it drifts). The API's design and pack format: [shaders.md](shaders.md).
**Experimental** ([stability.md](stability.md)): `horizon-shader check` takes 0.2 packs, but the game refuses them
for now and the registry doesn't list them, and anything here may change in any release. The design:
[hdr-linear-light.md](../design/hdr-linear-light.md).
**Experimental** ([stability.md](stability.md)): `horizon-shader check` takes 0.2 packs, the game draws them only
with `HORIZON_HDR_FRAME=float` set, the registry doesn't list them, and anything here may change in any release.
The design: [hdr-linear-light.md](../design/hdr-linear-light.md).
## Colour
+7 -4
View File
@@ -155,9 +155,10 @@ the same way every time (files in path order, fixed dates), so a pack's SHA-256
## Shader API 0.2 (experimental)
0.2 is the line for linear light and HDR ([hdr-linear-light.md](../design/hdr-linear-light.md)). A pack opts in with
`api = "0.2"`. `horizon-shader check` and `pack` take it, but **the game refuses 0.2 packs for now and the registry
doesn't list them**: the game draws 0.2 in a later step, behind `HORIZON_HDR_FRAME=float` at first. What it changes
from 0.1:
`api = "0.2"`. `horizon-shader check` and `pack` take it; **the game draws 0.2 packs only with
`HORIZON_HDR_FRAME=float` set, and the registry doesn't list them**. With the switch, a renderer made with a 0.2 pack
picked draws the world into a float frame where the device can (else the 8-bit one), and a 0.2 pack picked or
dropped later runs on the frame there is. What it changes from 0.1:
- **Linear light.** Every colour a stage reads or returns is linear light, 1.0 at paper white and more brighter. Horizon
decodes vanilla's values with a 2.2 power and encodes the results the frame stores. Depth, normals, light levels,
@@ -196,7 +197,7 @@ refused. The example's port is `crates/horizon-shader-api/packs/golden_hour_0_2`
## Stability
Experimental (API 0.1). The stages ship in the game behind pack selection; see [stability.md](stability.md). API
0.2 is experimental too, and the game doesn't draw it yet.
0.2 is experimental too, and the game draws it only with `HORIZON_HDR_FRAME=float`.
## Not there yet
@@ -205,6 +206,8 @@ and particles (the material stage covers terrain only).
## Changelog
- 2026-11-05: API 0.2 (change, experimental): the game draws 0.2 packs with `HORIZON_HDR_FRAME=float` set, on a
float frame where the device can; `[frame] blend = "linear"` is still refused. API 0.1 is unchanged.
- 2026-11-05: API 0.2 (new line, experimental): linear light, `Frame`'s display fields, `[tonemap]` and
`[frame] blend`, with its own reference. `horizon-shader` checks and packs it; the game and the registry refuse it
for now. API 0.1 is unchanged: the uploaded `Frame` buffer grows by 32 bytes after the fields 0.1 declares.
+1 -1
View File
@@ -91,7 +91,7 @@ Deprecations and breaking changes are in bold. The release notes of the game rel
| Native mods (horizon-mixins bundles, [native-mods.md](../native-mods.md)) | experimental | the pinned revisions |
| `cargo horizon-mod` (`new`, `build`, `run`, `check`) | preview | none yet (unversioned) |
| Shader API | experimental | 0.1 |
| Shader API 0.2 (linear light, [hdr-linear-light.md](../design/hdr-linear-light.md)) | experimental, behind a switch: `horizon-shader` checks it, the game refuses it until it draws it behind `HORIZON_HDR_FRAME=float`, and the registry doesn't list it | 0.2 |
| Shader API 0.2 (linear light, [hdr-linear-light.md](../design/hdr-linear-light.md)) | experimental, behind a switch: `horizon-shader` checks it, the game draws it only with `HORIZON_HDR_FRAME=float` set, and the registry doesn't list it | 0.2 |
| `horizon-ctl` commands and MCP tools | preview | none yet (unversioned) |
| The game's own control-socket commands outside `horizon-ctl`'s table (test hooks) | experimental | |
| `horizon-server` commands, `server.toml` | stable | `horizon-server --version` |