mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
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:
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!(
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`.
|
||||
@@ -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,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
@@ -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.
|
||||
|
||||
@@ -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` |
|
||||
|
||||
Reference in new issue
Block a user