5 Commits

Author SHA1 Message Date
chexit ed751714e0 Add feature-gated DLSS Super Resolution backend (#124)
Compose NGX and HDR Vulkan device creation, retain one SDK per device and a context per output size, and evaluate DLSS with existing motion/depth/jitter. Bracket NGX with explicit display-linear color conversion and ordered command buffers. Native TAA/TAAU remains the fallback; Windows compile check passes, runtime acceptance still requires RTX.

Co-Authored-By: Kimi Code <noreply@moonshot.ai>
2026-11-14 22:09:21 +03:00
chexit 7a73e8d26a Record previous mod-model transforms for temporal motion (#124)
Keep each mod model's last rendered world transform and emit a separate previous-position vertex stream. A depth-read-only motion pass overwrites camera-only vectors with exact previous-frame object motion and matching alpha cutouts. Add HORIZON_HD_DEBUG_MOTION for visual verification.

Co-Authored-By: Kimi Code <noreply@moonshot.ai>
2026-11-14 21:04:21 +03:00
chexit 17382445af Blend HD shadow filters across the near cascade boundary
The near colour-shadow pass leaves leaves out of depth while the far cascade includes them. Blend both filters over six blocks and fade tint on the same curve so the boundary no longer reads as a circle around the camera.

Co-Authored-By: Kimi Code <noreply@moonshot.ai>
2026-11-14 18:03:26 +03:00
chexit c962cd472f Record dense camera motion for temporal upscaling (#124)
Add an RG16F current-to-previous render-pixel target to the HD G-buffer. Terrain and entities write previous drawn-camera projections; TAA/TAAU use matching opaque vectors and retain the depth fallback elsewhere. Object-local previous transforms and vendor SDK evaluation remain separate phases.

Co-Authored-By: Kimi Code <noreply@moonshot.ai>
2026-11-14 17:46:38 +03:00
chexit 94b572cd24 Uploads: round oversized staging allocations up to a power of two
Uploads staged every buffer write through wgpu's StagingBelt, which gives a
write larger than its chunk a buffer of exactly that size, keeps it for good
and only reuses it for writes that fit. The entity batch outgrows the 1 MiB
chunk with a few hundred mobs and grows a little each tick while more appear,
so every tick took a new buffer: about 1 GB more per hundred mobs past ~750,
never given back, until the game was killed (upstream #2826).

Writes larger than the chunk now take a power-of-two slice and copy only
their bytes, so the buffers the belt keeps are reused: one per doubling.

Backport of horizon-rust/horizon@f7b77fbc88,
whose file moved from crates/horizon-client-render/src/gfx/upload.rs to
crates/horizon-client/src/gfx/upload.rs. Takes its growth test
(growing_large_writes_reuse_the_belt_buffers) and the kept_buffers helper as
written. Fork issue: #130.

Co-Authored-By: Kimi Code <noreply@moonshot.cn>
2026-11-14 17:22:37 +03:00
19 changed files with 1157 additions and 89 deletions
Generated
+93 -15
View File
@@ -195,7 +195,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.5",
]
[[package]]
@@ -223,6 +223,25 @@ dependencies = [
"horizon-mod-sdk",
]
[[package]]
name = "bindgen"
version = "0.73.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "787ef8ef523575546b106a58213d6e6b06198a05c2f757258c68a74273670cfa"
dependencies = [
"bitflags 2.13.2",
"cexpr",
"clang-sys",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash 2.1.3",
"shlex",
"syn 2.0.119",
]
[[package]]
name = "bit-set"
version = "0.10.0"
@@ -303,7 +322,7 @@ checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.5",
]
[[package]]
@@ -364,6 +383,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom 7.1.3",
]
[[package]]
name = "cfg-if"
version = "1.0.5"
@@ -422,6 +450,17 @@ dependencies = [
"zeroize",
]
[[package]]
name = "clang-sys"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "clipboard-win"
version = "5.4.1"
@@ -805,7 +844,7 @@ checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.5",
]
[[package]]
@@ -843,7 +882,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.5",
]
[[package]]
@@ -855,6 +894,21 @@ dependencies = [
"libloading",
]
[[package]]
name = "dlss_wgpu"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6d49749f048ae6512153417e1ca603417620f6024132a4bb6d7a52f138a8bea"
dependencies = [
"ash",
"bindgen",
"bitflags 2.13.2",
"cc",
"thiserror 2.0.21",
"uuid",
"wgpu",
]
[[package]]
name = "document-features"
version = "0.2.12"
@@ -1017,7 +1071,7 @@ checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.5",
]
[[package]]
@@ -1223,6 +1277,12 @@ version = "0.30.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9"
[[package]]
name = "glob"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
[[package]]
name = "glow"
version = "0.17.0"
@@ -1379,6 +1439,7 @@ dependencies = [
"bytemuck",
"chrono",
"cpal",
"dlss_wgpu",
"flate2",
"gilrs",
"glam",
@@ -1415,6 +1476,7 @@ dependencies = [
"symphonia",
"unicode-bidi",
"ureq",
"uuid",
"wgpu",
"winit",
"winresource",
@@ -2177,6 +2239,12 @@ dependencies = [
"libc",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "minimap"
version = "1.0.0"
@@ -2297,6 +2365,16 @@ dependencies = [
"libc",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "nom"
version = "8.0.0"
@@ -3338,7 +3416,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.5",
]
[[package]]
@@ -3620,9 +3698,9 @@ dependencies = [
[[package]]
name = "syn"
version = "3.0.6"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
dependencies = [
"proc-macro2",
"quote",
@@ -3637,7 +3715,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.5",
]
[[package]]
@@ -3692,7 +3770,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.5",
]
[[package]]
@@ -3814,7 +3892,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6"
dependencies = [
"memchr",
"nom",
"nom 8.0.0",
"petgraph",
]
@@ -4005,7 +4083,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.5",
"wasm-bindgen-shared",
]
@@ -5295,7 +5373,7 @@ checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.5",
"synstructure",
]
@@ -5336,7 +5414,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.5",
"synstructure",
]
@@ -5376,7 +5454,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.5",
]
[[package]]
+7
View File
@@ -86,8 +86,15 @@ wgpu = { version = "30.0.1", default-features = false, features = ["gles"] }
# The icon resource in the .exe (build.rs).
winresource = "0.1"
[target.'cfg(any(windows, linux))'.dependencies]
# Optional NVIDIA Super Resolution PoC; NGX SDK is needed only when this feature is built.
dlss_wgpu = { version = "6.0.0", optional = true }
uuid = { version = "1", optional = true }
[features]
default = ["audio", "controllers"]
# Native DLSS Super Resolution over Vulkan. Off by default: no proprietary SDK is needed for normal builds.
dlss = ["dep:dlss_wgpu", "dep:uuid"]
# Sound output. Without it (and `controllers`) the program links neither libasound nor libudev on
# Linux: the server build (`--no-default-features`, released as horizon-engine; docs/server.md).
audio = ["dep:cpal"]
+6
View File
@@ -4,6 +4,12 @@ fn main() {
let dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
let ico = format!("{dir}/../../packaging/icons/horizon.ico");
println!("cargo:rerun-if-changed={ico}");
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows")
&& std::env::var("CARGO_FEATURE_DLSS").is_ok()
{
// dlss_wgpu 6.0.0 leaves out nvsdk_ngx's registry import library on Windows.
println!("cargo:rustc-link-lib=advapi32");
}
#[cfg(windows)]
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") {
let mut resource = winresource::WindowsResource::new();
+32
View File
@@ -28,6 +28,11 @@ struct Globals {
hd_wind: vec4<f32>,
// AirDef fork: the sprite layers' material classes, 4 bits each (gfx/hd/materials.rs).
hd_materials: array<vec4<u32>, 64>,
// Previous drawn (jittered) view-projection and the camera's translation since it.
prev_view_proj: mat4x4<f32>,
camera_delta: vec4<f32>,
// Render-target width and height in pixels for current-to-previous motion vectors.
hd_motion: vec4<f32>,
}
@group(0) @binding(0) var<uniform> globals: Globals;
@group(0) @binding(1) var atlas: texture_2d<f32>;
@@ -65,6 +70,8 @@ struct Out {
// AirDef fork, HD lighting's G-buffer: the camera-relative position, sky and block light.
@location(13) pos: vec3<f32>,
@location(14) lm: vec2<f32>,
// Camera motion only; previous object-local transforms are supplied by a later pass.
@location(15) prev_clip: vec4<f32>,
}
// Shipped light.glsl; normals are already transformed and RGBA8_SNORM packed.
fn entity_light(normal: vec3<f32>, nether: bool) -> f32 {
@@ -98,6 +105,7 @@ fn entity_vertex(position: vec3<f32>, uv: vec2<f32>,
overlay: u32, color: vec4<f32>, light: vec4<u32>, normal: vec4<f32>) -> Out {
var out: Out;
out.clip = globals.view_proj * vec4(position, 1.0);
out.prev_clip = globals.prev_view_proj * vec4(position + globals.camera_delta.xyz, 1.0);
out.proj = vec4(out.clip.xy * 0.5 + out.clip.w * 0.5, out.clip.zw);
out.uv = uv;
let l = vec2(max(f32(light.y), dynamic_block_light(position)), f32(light.x));
@@ -140,6 +148,15 @@ fn entity_vertex(position: vec3<f32>, uv: vec2<f32>,
out.glint_uv = glint_uv;
return out;
}
@vertex fn vs_entity_motion(@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>,
@location(2) overlay: u32, @location(3) color: vec4<f32>, @location(4) light: vec4<u32>,
@location(5) normal: vec4<f32>, @location(6) glint_uv: vec2<f32>,
@location(7) previous: vec3<f32>) -> Out {
var out = entity_vertex(position, uv, overlay, color, light, normal);
out.glint_uv = glint_uv;
out.prev_clip = globals.prev_view_proj * vec4(previous + globals.camera_delta.xyz, 1.0);
return out;
}
// Retained bolt-local geometry, translated using the same f64 camera
// subtraction as the former CPU path before adding each local f32 position.
// The bolt offset is instance attribute 7: 6 is the entity vertex's glint UV.
@@ -278,6 +295,20 @@ struct EntityGBufferOut {
@location(0) albedo: vec4<f32>,
@location(1) normal: vec4<f32>,
@location(2) light: vec4<f32>,
@location(3) motion: vec2<f32>,
}
fn motion_pixels(pos: vec3<f32>, previous: vec4<f32>) -> vec2<f32> {
let current = globals.view_proj * vec4(pos, 1.0);
if current.w <= 0.0 || previous.w <= 0.0 { return vec2<f32>(0.0); }
let c = current.xy / current.w;
let p = previous.xy / previous.w;
let delta = vec2<f32>((p.x - c.x) * 0.5, (c.y - p.y) * 0.5);
return delta * globals.hd_motion.xy;
}
@fragment fn fs_entity_motion(in: Out, @builtin(front_facing) front: bool) -> @location(0) vec2<f32> {
let color = textureSample(atlas, atlas_sampler, in.uv) * select(in.back_color, in.color, front);
if color.a < 0.1 || (in.cull != 0u && !front) || in.overlay == 4u || in.overlay == 9u || in.overlay == 10u { discard; }
return motion_pixels(in.pos, in.prev_clip);
}
@fragment fn fs_entity_gbuffer(in: Out, @builtin(front_facing) front: bool) -> EntityGBufferOut {
let color = textureSample(atlas, atlas_sampler, in.uv) * select(in.back_color, in.color, front);
@@ -296,6 +327,7 @@ struct EntityGBufferOut {
if in.overlay == 42u { material = vec2(0.28, 1.0); }
if in.overlay == 40u { material = vec2(0.5, 0.0); }
out.light = vec4(clamp(in.lm, vec2(0.0), vec2(1.0)), material);
out.motion = motion_pixels(in.pos, in.prev_clip);
return out;
}
// AirDef fork: entities in the sun's shadow map.
+10
View File
@@ -11989,6 +11989,10 @@ impl Game<'_> {
hud.patches.append(&mut self.map_patches);
let entity_spans = std::mem::take(self.entity_spans.get_mut());
let glowing = std::mem::take(self.glowing_vertices.get_mut());
#[cfg(feature = "mods")]
let motion_models = std::mem::take(&mut self.mods.models.motion);
#[cfg(not(feature = "mods"))]
let motion_models = Vec::new();
// The main view is the frame itself, not a picture of its own.
let mod_views: Vec<_> = self
.mod_views(alpha)
@@ -12100,6 +12104,7 @@ impl Game<'_> {
color: sky.cloud_color,
}),
entities: &entities,
motion_models: &motion_models,
entity_spans: &entity_spans,
glowing: &glowing,
dynamic_lights: &self.dynamic_lights,
@@ -12166,6 +12171,10 @@ impl Game<'_> {
}
// Restore storage even when rendering fails (for example, a lost surface).
self.entity_vertices = entities;
#[cfg(feature = "mods")]
{
self.mods.models.motion = motion_models;
}
*self.entity_spans.get_mut() = entity_spans;
*self.glowing_vertices.get_mut() = glowing;
self.see_through_vertices = see_through;
@@ -15021,6 +15030,7 @@ fn loading_frame(hud: &HudBatch) -> Frame<'_> {
glint_ticks: 0.0,
nether_lighting: false,
entities: &[],
motion_models: &[],
entity_spans: &[],
glowing: &[],
dynamic_lights: &[],
+39 -3
View File
@@ -7,7 +7,7 @@ use super::Game;
use {
glam::{DVec3, Mat4, Quat, Vec3},
horizon_client::{
mesher::Vertex,
mesher::{MotionVertex, Vertex},
particles::{ParticleOptions, ParticleType},
},
horizon_core::random::RandomSource,
@@ -16,8 +16,18 @@ use {
std::collections::HashMap,
};
/// The last transform a frame actually drew, independent of the camera.
#[cfg(feature = "mods")]
#[derive(Clone, Copy)]
struct RenderedPose {
at: DVec3,
rotation: Quat,
scale: f32,
}
/// One model of a mod: where it was a tick ago and where it is now.
#[cfg(feature = "mods")]
#[derive(Clone, Copy)]
struct Pose {
state: BlockStateId,
previous: DVec3,
@@ -27,6 +37,7 @@ struct Pose {
old_scale: f32,
scale: f32,
glow: bool,
rendered: Option<RenderedPose>,
}
/// The models the running mods keep, by (mod, id).
@@ -34,6 +45,8 @@ struct Pose {
#[derive(Default)]
pub(super) struct ModModels {
poses: HashMap<(usize, u32), Pose>,
/// Current and previous-frame vertices for the HD motion-only pass.
pub(super) motion: Vec<MotionVertex>,
/// Block states by the text mods name them, parsed once.
states: HashMap<String, Option<BlockStateId>>,
/// The version of the mod post pass the renderer has (0: none).
@@ -158,6 +171,7 @@ impl Game<'_> {
old_scale: m.scale,
scale: m.scale,
glow: m.glow,
rendered: None,
},
);
}
@@ -389,10 +403,12 @@ impl Game<'_> {
/// Draws every mod model between its last two poses, lit by the world
/// (or at full brightness), its faces shaded by its own rotation.
pub(super) fn mod_model_geometry(&self, camera: DVec3, alpha: f64, out: &mut Vec<Vertex>) {
pub(super) fn mod_model_geometry(&mut self, camera: DVec3, alpha: f64, out: &mut Vec<Vertex>) {
self.mods.models.motion.clear();
if self.mods.models.poses.is_empty() {
return;
}
let motion = self.mod_hd().is_some();
let t = alpha.clamp(0.0, 1.0);
// Missiles are seen from far off: past the far plane a model is drawn nearer and
// smaller, the same size on screen.
@@ -422,10 +438,11 @@ impl Game<'_> {
Some(([r.u0, r.v0, r.u1, r.v1], m))
})
.collect();
for p in self.mods.models.poses.values() {
for (key, p) in self.mods.models.poses.iter().map(|(key, pose)| (*key, *pose)).collect::<Vec<_>>() {
let mut at = p.previous.lerp(p.position, t);
let d = at.distance(camera);
if d > 4096.0 {
self.mods.models.poses.get_mut(&key).unwrap().rendered = None;
continue;
}
let mut shrink = 1.0f32;
@@ -436,8 +453,11 @@ impl Game<'_> {
let rotation = p.old_rotation.slerp(p.rotation, t as f32);
let scale = (p.old_scale + (p.scale - p.old_scale) * t as f32) * shrink;
if scale <= 0.0 {
self.mods.models.poses.get_mut(&key).unwrap().rendered = None;
continue;
}
let rendered = RenderedPose { at, rotation, scale };
let previous = p.rendered.unwrap_or(rendered);
let light = if p.glow {
GLOW
} else {
@@ -481,6 +501,22 @@ impl Game<'_> {
}
}
}
if motion {
let previous_matrix = Mat4::from_scale_rotation_translation(
Vec3::splat(previous.scale),
previous.rotation,
(previous.at - camera).as_vec3(),
) * Mat4::from_translation(Vec3::splat(-0.5));
let mut old = Vec::with_capacity(out.len() - start);
self.block_model_vertices(p.state, 0, previous_matrix, light, true, &mut old);
self.mods.models.motion.extend(
out[start..]
.iter()
.zip(old)
.map(|(current, previous)| MotionVertex::new(*current, previous.pos)),
);
}
self.mods.models.poses.get_mut(&key).unwrap().rendered = Some(rendered);
}
}
+181 -6
View File
@@ -6,7 +6,7 @@ use crate::{
environment::Sky,
graphics::GraphicsApi,
hud::{HudBatch, HudRenderer},
mesher::{SectionMesh, Sides, TerrainVertex, Vertex},
mesher::{MotionVertex, SectionMesh, Sides, TerrainVertex, Vertex},
sprites::SpriteAnimation,
};
use glam::{DVec3, Mat4, Vec3, Vec4};
@@ -69,6 +69,8 @@ pub fn outline_vertices(edges: &[[f64; 6]], camera: glam::DVec3) -> Vec<[f32; 6]
mod animate;
mod arena;
mod cull;
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
mod dlss;
mod end_sky;
mod flat;
mod hd;
@@ -209,6 +211,12 @@ struct Globals {
hd_wind: [f32; 4],
/// AirDef fork: the sprite layers' material classes, 4 bits each (`hd::materials`).
hd_materials: hd::MaterialTable,
/// The previous frame's drawn (jittered) view-projection, for dense motion vectors.
prev_view_proj: [[f32; 4]; 4],
/// xyz: the camera's translation since that frame.
camera_delta: [f32; 4],
/// Render-target width and height in pixels for current-to-previous motion vectors.
hd_motion: [f32; 4],
}
pub type SectionKey = (i32, i32, i32);
struct SectionGpu {
@@ -309,6 +317,8 @@ pub struct Frame<'a> {
pub nether_lighting: bool,
/// Camera-relative entity geometry textured from the shared atlas.
pub entities: &'a [Vertex],
/// Mod models with their previous drawn transforms, for the HD motion-only pass.
pub motion_models: &'a [MotionVertex],
/// The glowing entities' silhouettes in their team colours (`OutlineBufferSource`),
/// outlined through walls after the hand.
pub glowing: &'a [Vertex],
@@ -413,6 +423,10 @@ pub struct Renderer {
window: Arc<Window>,
pub device: wgpu::Device,
pub queue: wgpu::Queue,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
dlss_supported: bool,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
dlss_sdk: Option<dlss::Sdk>,
pub config: wgpu::SurfaceConfiguration,
solid: wgpu::RenderPipeline,
translucent: wgpu::RenderPipeline,
@@ -599,6 +613,9 @@ pub struct Renderer {
/// The water quads alone into the HD water targets (section draws; `flat.water` for flat ones).
gbuffer_water: wgpu::RenderPipeline,
gbuffer_entity: wgpu::RenderPipeline,
gbuffer_motion_models: wgpu::RenderPipeline,
motion_models_buffer: wgpu::Buffer,
motion_models_capacity: u64,
/// The HD sun shadows: depth only, from the light, and the sections in its farther cascade.
shadow_terrain: wgpu::RenderPipeline,
/// The near cascade's depth without leaves, and its colour map's leaves and glass
@@ -888,6 +905,10 @@ struct Opened {
adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
dlss_supported: bool,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
dlss_sdk: Option<dlss::Sdk>,
}
/// Logs the adapter and records it for telemetry and crash reports.
fn describe_adapter(adapter: &wgpu::Adapter) {
@@ -965,7 +986,24 @@ async fn open_device(
if attempt > 0 {
log::warn!("Horizon renderer: trying {name} ({})", failures.join("; "));
}
let instance = wgpu::Instance::new(instance_descriptor(backends));
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
let mut dlss_support = dlss_wgpu::FeatureSupport::default();
let instance = {
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
if dlss::requested() && backends == wgpu::Backends::VULKAN {
match dlss::create_instance(&instance_descriptor(backends), &mut dlss_support) {
Ok(instance) => instance,
Err(e) => {
failures.push(format!("{name} DLSS instance: {e}"));
continue;
}
}
} else {
wgpu::Instance::new(instance_descriptor(backends))
}
#[cfg(not(all(feature = "dlss", any(windows, target_os = "linux"))))]
wgpu::Instance::new(instance_descriptor(backends))
};
let surface = if offscreen {
None
} else {
@@ -1040,6 +1078,14 @@ async fn open_device(
Some(opened) => opened,
None => adapter.request_device(&desc).await,
};
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
let opened = if dlss::requested() && info.backend == wgpu::Backend::Vulkan {
dlss::request_device(&adapter, &desc, &mut dlss_support)
} else {
opened.map_err(|e| e.to_string())
};
#[cfg(not(all(feature = "dlss", any(windows, target_os = "linux"))))]
let opened = opened.map_err(|e| e.to_string());
match opened {
Ok((device, queue)) => {
return Ok(Opened {
@@ -1048,6 +1094,10 @@ async fn open_device(
adapter,
device,
queue,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
dlss_supported: dlss_support.super_resolution_supported,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
dlss_sdk: None,
});
}
Err(e) => failures.push(format!("{name} device: {e}")),
@@ -1145,6 +1195,10 @@ impl Renderer {
adapter,
device,
queue,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
dlss_supported,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
dlss_sdk,
} = match previous {
// Reload resources on the same device; replacing live Vulkan
// devices can invalidate the driver's swapchain state on the same
@@ -1170,10 +1224,26 @@ impl Renderer {
adapter: renderer.adapter.clone(),
device: renderer.device.clone(),
queue: renderer.queue.clone(),
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
dlss_supported: renderer.dlss_supported,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
dlss_sdk: renderer.dlss_sdk.clone(),
}
}
None => open_device(&window, offscreen, api, layers).await?,
};
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
let dlss_sdk = if dlss_sdk.is_some() || !dlss_supported || !dlss::requested() {
dlss_sdk
} else {
match dlss_wgpu::DlssSdk::new(dlss::PROJECT_ID, device.clone()) {
Ok(sdk) => Some(sdk),
Err(e) => {
log::warn!("DLSS unavailable, using native TAAU: {e}");
None
}
}
};
let info = adapter.get_info();
crate::graphics::suggest_reduced_far_detail(crate::graphics::lod_capped(&info));
let size = window.inner_size();
@@ -1425,6 +1495,11 @@ impl Renderer {
blend: None,
write_mask: wgpu::ColorWrites::ALL,
}),
Some(wgpu::ColorTargetState {
format: hd::MOTION,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
}),
];
// The shadow maps: plain depth from the light, biased against acne, both faces.
let shadow_depth = Some(wgpu::DepthStencilState {
@@ -2170,6 +2245,12 @@ impl Renderer {
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &entity_attributes,
})];
let motion_attributes = wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x2,2=>Uint32,3=>Unorm8x4,4=>Uint8x4,5=>Snorm8x4,6=>Float32x2,7=>Float32x3];
let motion_vertex = [Some(wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<MotionVertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &motion_attributes,
})];
#[allow(clippy::too_many_arguments)]
let entity_pipeline_with = |label,
cull_mode,
@@ -2241,6 +2322,37 @@ impl Renderer {
multiview_mask: None,
cache: None,
});
let gbuffer_motion_models = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("HD motion models"),
layout: Some(&entity_layout),
vertex: wgpu::VertexState {
module: &entity_shader,
entry_point: Some("vs_entity_motion"),
compilation_options: Default::default(),
buffers: &motion_vertex,
},
primitive: Default::default(),
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Equal),
stencil: Default::default(),
bias: Default::default(),
}),
multisample: Default::default(),
fragment: Some(wgpu::FragmentState {
module: &entity_shader,
entry_point: Some("fs_entity_motion"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: hd::MOTION,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
});
let shadow_entity = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("HD shadows entities"),
layout: Some(&entity_layout),
@@ -2759,6 +2871,13 @@ impl Renderer {
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let motion_models_capacity = 1 << 10;
let motion_models_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Motion model vertices"),
size: motion_models_capacity * std::mem::size_of::<MotionVertex>() as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let flat_wanted = flat::wanted(&adapter, &device);
// Readable as storage where flat draws run: a shader pack's terrain program reads them.
let arena = arena::Arena::new(
@@ -2879,6 +2998,10 @@ impl Renderer {
window,
device,
queue,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
dlss_supported,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
dlss_sdk,
config,
solid,
translucent,
@@ -3003,6 +3126,9 @@ impl Renderer {
hd_materials,
gbuffer_water,
gbuffer_entity,
gbuffer_motion_models,
motion_models_buffer,
motion_models_capacity,
shadow_terrain,
shadow_terrain_near,
shadow_tint_leaves,
@@ -4196,6 +4322,7 @@ impl Renderer {
label: Some("Horizon frame"),
})
});
let mut temporal_commands = Vec::new();
if self.animated_textures {
self.animate(&mut encoder, frame.ticks);
self.hud
@@ -4346,6 +4473,15 @@ impl Renderer {
[0.0; 4]
},
hd_materials: self.hd_materials,
prev_view_proj: taa_prev.map_or(view_proj, |p| p.2).to_cols_array_2d(),
camera_delta: {
let d = taa_prev.map_or(Vec3::ZERO, |p| (frame.camera - p.1).as_vec3());
[d.x, d.y, d.z, 0.0]
},
hd_motion: {
let size = self.world_post.size().unwrap_or([self.config.width, self.config.height]);
[size[0] as f32, size[1] as f32, 0.0, 0.0]
},
};
self.uploads
.buffer(&mut encoder, &self.globals, 0, bytemuck::bytes_of(&globals));
@@ -5110,6 +5246,24 @@ impl Renderer {
bytemuck::cast_slice(all_entities),
);
}
let motion_vertices = frame.motion_models.len() as u64;
if motion_vertices > self.motion_models_capacity {
self.motion_models_capacity = motion_vertices.next_power_of_two();
self.motion_models_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Motion model vertices"),
size: self.motion_models_capacity * std::mem::size_of::<MotionVertex>() as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
}
if motion_vertices > 0 {
self.uploads.buffer(
&mut encoder,
&self.motion_models_buffer,
0,
bytemuck::cast_slice(frame.motion_models),
);
}
self.entity_outline.prepare(
&self.device,
&mut self.uploads,
@@ -5365,10 +5519,21 @@ impl Renderer {
let format = self.config.format.remove_srgb_suffix();
let device = &self.device;
let globals_size = std::mem::size_of::<Globals>() as u64;
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
let hd = self.hd.get_or_insert_with(|| {
hd::Hd::new(device, &self.queue, size, format, globals_size, self.dlss_sdk.clone())
});
#[cfg(not(all(feature = "dlss", any(windows, target_os = "linux"))))]
let hd = self.hd.get_or_insert_with(|| {
hd::Hd::new(device, &self.queue, size, format, globals_size)
});
hd.resize(device, size, [self.config.width, self.config.height]);
hd.resize(
device,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
&self.queue,
size,
[self.config.width, self.config.height],
);
// Translucent terrain on screen: the water pass has something to draw.
hd.water_geometry = match self.flat.as_ref().filter(|_| flat_frame) {
Some(flat) => !flat.translucent_calls().is_empty(),
@@ -5587,6 +5752,14 @@ impl Renderer {
pass.draw_indexed(0..entity_quads * 6, 0, 0..1);
}
drop(pass);
if motion_vertices > 0 {
let mut pass = hd.motion_models_pass(&mut encoder);
pass.set_pipeline(&self.gbuffer_motion_models);
pass.set_bind_group(0, &self.entity_group, &[]);
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
pass.set_vertex_buffer(0, self.motion_models_buffer.slice(..));
pass.draw_indexed(0..motion_vertices as u32 / 4 * 6, 0, 0..1);
}
// Contact shadows from the G-buffer's depth and normals, for the lighting.
hd.ao(&mut encoder);
// The water quads alone, with their own depth: the lighting finds the water by it and
@@ -6169,12 +6342,12 @@ impl Renderer {
self.mod_params.1,
);
post.run(&self.device, &mut encoder, (hd.lit_view(), depth), &hd.fx);
let source = hd.taa(&self.device, &mut encoder, hd::HdSource::Fx, depth);
let source = hd.taa(&self.device, &self.adapter, &mut encoder, &mut temporal_commands, hd::HdSource::Fx, depth);
let source = hd.film(&self.device, &mut encoder, source, depth);
hd.finish(&self.device, &mut encoder, source, &view_target);
}
_ => {
let source = hd.taa(&self.device, &mut encoder, hd::HdSource::Lit, depth);
let source = hd.taa(&self.device, &self.adapter, &mut encoder, &mut temporal_commands, hd::HdSource::Lit, depth);
let source = hd.film(&self.device, &mut encoder, source, depth);
hd.finish(&self.device, &mut encoder, source, &view_target);
}
@@ -6361,7 +6534,9 @@ impl Renderer {
startup_step(format_args!("submitting the first frame"));
self.first_frame = FirstFrame::Drawn;
}
let submission = crate::spans::time("submit", || self.queue.submit([commands]));
let submission = crate::spans::time("submit", || {
self.queue.submit(std::iter::once(commands).chain(temporal_commands))
});
if let Some(cull) = &mut self.cull {
cull.submitted();
}
+38
View File
@@ -0,0 +1,38 @@
//! Optional NVIDIA DLSS bootstrap. The rest of the renderer is independent of the SDK.
use std::sync::Arc;
use uuid::Uuid;
use wgpu::{Adapter, Device, DeviceDescriptor, Instance, InstanceDescriptor, Queue};
pub const PROJECT_ID: Uuid = Uuid::from_u128(0x7c35_0b91_38a4_4f8e_8c4a_66f2_5df9_1240);
pub fn requested() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| std::env::var("HORIZON_DLSS").is_ok_and(|v| v == "1"))
}
pub fn create_instance(
desc: &InstanceDescriptor,
support: &mut dlss_wgpu::FeatureSupport,
) -> Result<Instance, String> {
dlss_wgpu::create_instance(PROJECT_ID, desc, support).map_err(|e| e.to_string())
}
pub fn request_device(
adapter: &Adapter,
desc: &DeviceDescriptor<'_>,
support: &mut dlss_wgpu::FeatureSupport,
) -> Result<(Device, Queue), String> {
let mut registration_error = None;
let opened = horizon_hdr::open_vulkan_device_with_callback(adapter, desc, |raw, args| {
if let Err(e) = dlss_wgpu::register_device_extensions(PROJECT_ID, args, raw, support) {
registration_error = Some(e.to_string());
}
})
.ok_or_else(|| "DLSS requires a Vulkan adapter".to_string())?;
if let Some(e) = registration_error {
return Err(e);
}
opened.map_err(|e| e.to_string())
}
pub type Sdk = Arc<std::sync::Mutex<dlss_wgpu::DlssSdk>>;
+232
View File
@@ -0,0 +1,232 @@
//! DLSS Super Resolution behind the native TAA interface. NGX never sees the display-space
//! colour used by Horizon: two small conversion passes bracket its command buffer.
use super::{entries, module, target, SAMPLER, UNFILTERED};
use dlss_wgpu::{
DlssFeatureFlags, DlssPerfQualityMode, DlssSdk,
super_resolution::{DlssSuperResolution, DlssSuperResolutionExposure, DlssSuperResolutionRenderParameters},
};
use std::sync::{Arc, Mutex};
const COLOR: &str = include_str!("dlss_color.wgsl");
const BINDINGS: [wgpu::BindingType; 2] = [UNFILTERED, SAMPLER];
struct ColorConvert {
input: wgpu::TextureView,
output: wgpu::TextureView,
layout: wgpu::BindGroupLayout,
linearize: wgpu::RenderPipeline,
delinearize: wgpu::RenderPipeline,
}
impl ColorConvert {
fn new(device: &wgpu::Device) -> Self {
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("HD DLSS colour"),
entries: &entries(&BINDINGS),
});
let module = module(device, "HD DLSS colour", COLOR);
let pipeline = |entry| {
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("HD DLSS colour"),
bind_group_layouts: &[Some(&layout)],
..Default::default()
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("HD DLSS colour"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &module,
entry_point: Some("vs_full"),
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some(entry),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: super::HDR,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
multiview_mask: None,
cache: None,
})
};
let mut this = Self {
input: target(device, "HD DLSS linear input", [1, 1], super::HDR).view,
output: storage_target(device, "HD DLSS linear output", [1, 1]),
layout: layout.clone(),
linearize: pipeline("fs_linearize"),
delinearize: pipeline("fs_delinearize"),
};
this.resize(device, [1, 1], [1, 1]);
this
}
fn resize(&mut self, device: &wgpu::Device, world: [u32; 2], out: [u32; 2]) {
self.input = target(device, "HD DLSS linear input", world, super::HDR).view;
self.output = storage_target(device, "HD DLSS linear output", out);
}
fn record(
&self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
pipeline: &wgpu::RenderPipeline,
source: &wgpu::TextureView,
target: &wgpu::TextureView,
sampler: &wgpu::Sampler,
) {
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("HD DLSS colour"),
layout: &self.layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(source),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
});
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("HD DLSS colour"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
..Default::default()
});
pass.set_pipeline(pipeline);
pass.set_bind_group(0, &group, &[]);
pass.draw(0..3, 0..1);
}
}
fn storage_target(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: super::HDR,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::STORAGE_BINDING,
view_formats: &[],
})
.create_view(&Default::default())
}
pub struct Dlss {
sdk: Arc<Mutex<DlssSdk>>,
context: Option<DlssSuperResolution>,
convert: ColorConvert,
world: [u32; 2],
out: [u32; 2],
failed: bool,
}
impl Dlss {
pub fn new(sdk: Arc<Mutex<DlssSdk>>, device: &wgpu::Device) -> Self {
Self {
sdk,
context: None,
convert: ColorConvert::new(device),
world: [1, 1],
out: [1, 1],
failed: false,
}
}
pub fn available(&self) -> bool {
self.context.is_some() && !self.failed
}
pub fn mark_failed(&mut self) {
self.failed = true;
}
pub fn resize(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, world: [u32; 2], out: [u32; 2]) {
if self.world == world && self.out == out {
return;
}
self.world = world;
self.out = out;
self.convert.resize(device, world, out);
let flags = DlssFeatureFlags::LowResolutionMotionVectors
| DlssFeatureFlags::InvertedDepth
| DlssFeatureFlags::HighDynamicRange
| DlssFeatureFlags::AutoExposure;
match DlssSuperResolution::new(
out,
DlssPerfQualityMode::Auto,
flags,
self.sdk.clone(),
device,
queue,
) {
Ok(context) => {
log::info!("DLSS Super Resolution: {world:?} -> {out:?}");
self.context = Some(context);
}
Err(e) => {
log::warn!("DLSS context creation failed, using native TAAU: {e}");
self.context = None;
}
}
}
pub fn render(
&mut self,
device: &wgpu::Device,
adapter: &wgpu::Adapter,
encoder: &mut wgpu::CommandEncoder,
after: &mut wgpu::CommandEncoder,
source: &wgpu::TextureView,
depth: &wgpu::TextureView,
motion: &wgpu::TextureView,
display_target: &wgpu::TextureView,
sampler: &wgpu::Sampler,
jitter: [f32; 2],
reset: bool,
) -> Result<wgpu::CommandBuffer, String> {
let Some(context) = &mut self.context else {
return Err("no DLSS context".into());
};
self.convert.record(device, encoder, &self.convert.linearize, source, &self.convert.input, sampler);
let params = DlssSuperResolutionRenderParameters {
color: &self.convert.input,
depth,
motion_vectors: motion,
exposure: DlssSuperResolutionExposure::Automatic,
bias: None,
dlss_output: &self.convert.output,
reset,
jitter_offset: jitter,
partial_texture_size: Some(self.world),
motion_vector_scale: Some([1.0, 1.0]),
};
let commands = context.render(params, encoder, adapter).map_err(|e| e.to_string())?;
self.convert.record(device, after, &self.convert.delinearize, &self.convert.output, display_target, sampler);
Ok(commands)
}
}
@@ -0,0 +1,21 @@
@group(0) @binding(0) var source: texture_2d<f32>;
@group(0) @binding(1) var linear: sampler;
fn to_linear(c: vec3<f32>) -> vec3<f32> {
let lo = c / 12.92;
let hi = pow((c + 0.055) / 1.055, vec3<f32>(2.4));
return select(hi, lo, c <= vec3<f32>(0.04045));
}
fn to_display(c: vec3<f32>) -> vec3<f32> {
let lo = c * 12.92;
let hi = 1.055 * pow(c, vec3<f32>(1.0 / 2.4)) - 0.055;
return select(hi, lo, c <= vec3<f32>(0.0031308));
}
@fragment fn fs_linearize(in: FullOut) -> @location(0) vec4<f32> {
let c = textureSampleLevel(source, linear, in.uv, 0.0);
return vec4<f32>(to_linear(max(c.rgb, vec3<f32>(0.0))), c.a);
}
@fragment fn fs_delinearize(in: FullOut) -> @location(0) vec4<f32> {
let c = textureSampleLevel(source, linear, in.uv, 0.0);
return vec4<f32>(to_display(max(c.rgb, vec3<f32>(0.0))), c.a);
}
+40 -1
View File
@@ -7,6 +7,9 @@
//! - `normal` (`Rgba16Float`): rgb world-space unit normal (from `dpdx/dpdy`; plants use up), a unused.
//! - `levels` (`Rgba8Unorm`): x = sky light, y = block light, both the raw lightmap coordinate / 240
//! (0..1); zw unused.
//! - `motion` (`Rg16Float`): current-to-previous displacement in render-target pixels, with both
//! projections as actually drawn, jitter included. Mod models overwrite camera/static motion with
//! their previous drawn transforms in `motion_models_pass`; vanilla entities and wind remain camera-only.
//! - `depth` (`Depth32Float`): camera depth, **reversed-Z** (cleared to 0 = far / nothing, 1 = near).
//! An untouched pixel (depth 0) is sky.
//! - Readers: AO, lighting, water composite, TAA/TAAU (depth). Written by `gbuffer_pass`; the caller
@@ -18,6 +21,8 @@ pub const ALBEDO: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
pub const NORMAL: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
/// Sky and block light per pixel.
pub const LIGHT_LEVELS: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
/// Current-to-previous motion in render-target pixels.
pub const MOTION: wgpu::TextureFormat = wgpu::TextureFormat::Rg16Float;
/// The camera depth formats (the G-buffer's and the water's own).
pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
@@ -25,6 +30,7 @@ pub(super) struct GBuffer {
pub(super) albedo: wgpu::TextureView,
pub(super) normal: wgpu::TextureView,
pub(super) levels: wgpu::TextureView,
pub(super) motion: wgpu::TextureView,
pub(super) depth: wgpu::TextureView,
}
@@ -35,13 +41,14 @@ impl GBuffer {
albedo: target(device, "HD albedo", size, ALBEDO).view,
normal: target(device, "HD normal", size, NORMAL).view,
levels: target(device, "HD light levels", size, LIGHT_LEVELS).view,
motion: target(device, "HD motion", size, MOTION).view,
depth: target(device, "HD depth", size, DEPTH_FORMAT).view,
}
}
}
impl Hd {
/// The G-buffer pass's attachments: albedo and normal cleared, its own depth.
/// The G-buffer pass's attachments: colour, normal, light and motion cleared, its own depth.
pub fn gbuffer_pass<'e>(
&mut self,
encoder: &'e mut wgpu::CommandEncoder,
@@ -65,6 +72,7 @@ impl Hd {
clear(&self.gbuffer.albedo),
clear(&self.gbuffer.normal),
clear(&self.gbuffer.levels),
clear(&self.gbuffer.motion),
],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.gbuffer.depth,
@@ -78,4 +86,35 @@ impl Hd {
})
.forget_lifetime()
}
/// Overwrite only motion for mod models with previous drawn transforms; depth stays read-only.
pub fn motion_models_pass<'e>(
&mut self,
encoder: &'e mut wgpu::CommandEncoder,
) -> wgpu::RenderPass<'e> {
encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("HD motion models"),
timestamp_writes: stamp(&mut self.timing, "hd motion models"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &self.gbuffer.motion,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.gbuffer.depth,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
..Default::default()
})
.forget_lifetime()
}
}
+20 -5
View File
@@ -119,11 +119,8 @@ fn soft_shadow(s: vec3<f32>, texel: f32) -> f32 {
return lit / 12.0;
}
// `shadow` with a (2 * r + 1) square of taps: the floor under water takes one (the water dims and
// blurs it anyway).
fn shadow_taps(p: vec3<f32>, n: vec3<f32>, r: i32) -> f32 {
fn shadow_filter(p: vec3<f32>, n: vec3<f32>, r: i32, c: i32) -> f32 {
let dist = length(p);
let c = select(1, 0, dist < frame.shadow.y * 0.92);
let texel = select(frame.shadow.z, frame.shadow.y, c == 0) * 2.0 / 2048.0;
let q = p + n * texel * 1.5;
let s = cascade_at(q, c);
@@ -142,6 +139,24 @@ fn shadow_taps(p: vec3<f32>, n: vec3<f32>, r: i32) -> f32 {
return mix(1.0, lit / f32((2 * r + 1) * (2 * r + 1)), fade);
}
// `shadow` with a (2 * r + 1) square of taps: the floor under water takes one (the water dims and
// blurs it anyway). The near colour-shadow pass leaves leaves out of its depth map, so switching
// straight to the far map would draw a circle round the camera. Blend both filters over the same
// six blocks where the colour tint fades out.
fn shadow_taps(p: vec3<f32>, n: vec3<f32>, r: i32) -> f32 {
let dist = length(p);
let reach = frame.shadow.y * 0.92;
if dist >= reach {
return shadow_filter(p, n, r, 1);
}
let near = shadow_filter(p, n, r, 0);
let blend = smoothstep(reach - 6.0, reach, dist);
if blend <= 0.0 {
return near;
}
return mix(near, shadow_filter(p, n, r, 1), blend);
}
// Coloured shadows: the colour the near cascade's glass and leaves give the sunlight that reaches
// p (white where it is clear). The map (`shadows.rs`) holds, per light-space texel, the colour
// multiplier of the quads there and the nearest one's distance from the light; a tap counts when
@@ -165,7 +180,7 @@ fn shadow_tint(p: vec3<f32>, n: vec3<f32>) -> vec3<f32> {
let in_front = smoothstep(0.0, 0.004, local - t.a - 0.003);
sum += mix(vec3<f32>(1.0), t.rgb, in_front);
}
return mix(vec3<f32>(1.0), sum / 8.0, clamp((reach - dist) / 6.0, 0.0, 1.0));
return mix(vec3<f32>(1.0), sum / 8.0, 1.0 - smoothstep(reach - 6.0, reach, dist));
}
// One shadow sample, for the rays through the air.
+61 -8
View File
@@ -24,15 +24,16 @@
//! |---|---|---|---|
//! | 1 | `sky` | frame | sky texture 128x72 (linear Rec. 709 radiance) |
//! | 2 | `shadow_pass` x2 (caller draws) | - | shadow array (normal Z, 1 = far) |
//! | 3 | `gbuffer_pass` (caller draws) | - | albedo (a = emission), normal, light levels, depth |
//! | 3 | `gbuffer_pass` (caller draws) | - | albedo (a = emission), normal, light levels, current→previous motion (render pixels), depth |
//! | 3a | `water_pass` (caller draws) | - | water colour (a = block light), normal (a = sky light), water depth |
//! | 3b | `ao` | G-buffer depth, normal | AO half size (x occlusion, y distance) |
//! | 3c | `volumes` | frame, sky, world depth, shadows, noise | clouds (rgb, a = transmittance), cloud shadow map, fog (rgb, a = transmittance) + ray length |
//! | 4 | `light` | world colour + depth, G-buffer, shadows, sky, AO, volumes, water depth | `lit` (a: 1 normal / 0.5 water / 0.6 in front of water), `air` |
//! | 4a | `water_composite` | `lit`, `air`, G-buffer depth, water targets, shadows, sky | `lit_water` |
//! | 5 | mod post pass (caller) | `lit_view()` + depth | `fx` |
//! | 6 | `taa` | `lit_view()` or `fx`, world depth, history | `taa` + next history (out) |
//! | 6a | `film` (film quality only) | the colour source, world depth | motion blur, then depth of field (out) |
//! | 6 | `taa` | `lit_view()` or `fx`, world depth, G-buffer motion/depth, history | `taa` + next history (out) |
//! | 6a | optional DLSS | source, motion/depth, jitter | linearize → NGX → delinearize (separate command buffers) |
//! | 6b | `film` (film quality only) | the colour source, world depth | motion blur, then depth of field (out) |
//! | 7 | `finish` | colour source (`HdSource`) | bloom chain, then the screen |
//!
//! - **Colour.** Everything between the lighting and the tone curve is in vanilla's display space
@@ -62,6 +63,8 @@
mod ao;
mod bloom;
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
mod dlss;
mod film;
mod frame;
mod gbuffer;
@@ -94,7 +97,7 @@ use volumes::HdVolumes;
use water::Water;
pub use frame::{HdLight, HdScene, HdSettings, HdSpot, MAX_SPOTS, light_of, vanilla_cloud_tint};
pub use gbuffer::{ALBEDO, LIGHT_LEVELS, NORMAL};
pub use gbuffer::{ALBEDO, LIGHT_LEVELS, MOTION, NORMAL};
pub use materials::{Table as MaterialTable, table as material_table, wind_enabled, wind_seconds};
pub use shadows::{CASCADES, SHADOW_TINT, cascades, color_shadows_enabled};
pub use smoke::{HdSmoke, smoke_enabled, smoke_possible};
@@ -417,6 +420,7 @@ impl Hd {
size: [u32; 2],
screen: wgpu::TextureFormat,
globals_size: u64,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))] dlss_sdk: Option<super::dlss::Sdk>,
) -> Self {
let frame = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("HD frame"),
@@ -462,7 +466,11 @@ impl Hd {
spots: Vec::new(),
wet: (0.0, std::time::Instant::now()),
started: std::time::Instant::now(),
taa: Taa::new(device),
taa: Taa::new(
device,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
dlss_sdk,
),
film: Film::new(device),
bloom: Bloom::new(device),
tonemap: Tonemap::new(device, screen),
@@ -475,13 +483,25 @@ impl Hd {
groups_seen: 0,
groups_last_frame: 0,
};
hd.resize(device, size, size);
hd.resize(
device,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
queue,
size,
size,
);
hd
}
/// The targets: the world's at `size` (the world target's), the output's (TAA history, bloom)
/// at `out`, which is bigger when the world is upscaled.
pub fn resize(&mut self, device: &wgpu::Device, size: [u32; 2], out: [u32; 2]) {
pub fn resize(
&mut self,
device: &wgpu::Device,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))] queue: &wgpu::Queue,
size: [u32; 2],
out: [u32; 2],
) {
let out = [out[0].max(size[0]), out[1].max(size[1])];
if self.size == size && self.out == out {
return;
@@ -492,7 +512,14 @@ impl Hd {
self.lighting.resize(device, size);
self.fx = target(device, "HD post", size, HDR).view;
self.water.resize(device, size);
self.taa.resize(device, out);
self.taa.resize(
device,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
queue,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
size,
out,
);
self.film.resize();
self.ao
.resize(device, size, &self.frame, &self.gbuffer, &self.groups_made);
@@ -554,6 +581,10 @@ impl Hd {
let night = scene.dark.clamp(0.0, 1.0);
self.water.active = scene.water && water_enabled() && self.water_geometry;
self.taa.active = scene.taa && taa_enabled();
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
{
self.taa.jitter = scene.jitter_px;
}
// A long jump of the camera (a teleport) starts the history over.
if !self.taa.active || scene.camera_delta.length() > 64.0 {
self.taa.history_ready = false;
@@ -864,6 +895,10 @@ mod tests {
];
all.extend(volumes::shader_sources());
all.extend(smoke::shader_sources());
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
all.push(("dlss colour", dlss::COLOR.to_owned(), &dlss::BINDINGS));
#[cfg(not(all(feature = "dlss", any(windows, target_os = "linux"))))]
all.push(("dlss colour", include_str!("dlss_color.wgsl").to_owned(), &[UNFILTERED, SAMPLER]));
all
}
@@ -1031,6 +1066,24 @@ mod tests {
}
}
#[test]
fn dlss_colour_conversion_round_trips_display_space() {
fn linear(c: f32) -> f32 {
if c <= 0.04045 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) }
}
fn display(c: f32) -> f32 {
if c <= 0.0031308 { c * 12.92 } else { 1.055 * c.powf(1.0 / 2.4) - 0.055 }
}
for c in [0.0, 0.0031308, 0.04045, 0.2, 0.8, 1.0, 4.0, 16.0] {
let round_trip = display(linear(c));
assert!((round_trip - c).abs() < 0.0001, "{c} became {round_trip}");
}
assert_eq!(linear(0.0), 0.0);
assert_eq!(linear(1.0), 1.0);
assert_eq!(display(0.0), 0.0);
assert!((display(1.0) - 1.0).abs() < 1e-6);
}
/// `HdFrame` in Rust and in the prelude list the same fields and take the same room.
#[test]
fn frame_layout_matches_the_prelude() {
+92 -9
View File
@@ -4,7 +4,9 @@
//! Contract:
//! - Reads: the colour source (`HdSource::Lit`/`Fx`, the lit world with water, or the mod's post
//! output; display space, world size), the *world* depth (reversed-Z, the pass's own depth from the
//! world pass, not the G-buffer's), and the history (output size, `Rgba16Float`). `HdFrame`:
//! world pass, not the G-buffer's), the history (output size, `Rgba16Float`), and the G-buffer's
//! motion/depth. Matching opaque pixels use explicit current-to-previous render-pixel motion;
//! other pixels keep camera/depth reprojection. `HdFrame`:
//! `prev_view_proj` (last frame's matrix; **with** jitter for `fs_taa`, **without** for `fs_taau`,
//! chosen in `Hd::write`), `taa` (xyz the camera's move since, w history usable), `taa_params`
//! (x history weight, y sharpening, zw jitter in world pixels).
@@ -14,14 +16,20 @@
//! - Jitter: Halton(2,3) in clip space (`jitter`), added to the world `view_projection` only while HD
//! draws; the public matrix stays unjittered. The hand and the HUD are drawn after tonemap.
//! - A history belongs to one camera and one output size: do not feed one history from two cameras.
//! - Optional DLSS: the same inputs go through display→linear conversion, NGX, then linear→display.
//! `submissions` is ordered world+linearize, NGX evaluate, delinearize; native TAA is the fallback.
use super::{
DEPTH, FLOAT, HDR, Hd, HdSource, SAMPLER, UNFILTERED, UNIFORM, entries, module, stamp,
};
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
use super::dlss;
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
use std::sync::{Arc, Mutex};
pub(super) const TAA: &str = include_str!("taa.wgsl");
/// Group 0 of `fs_taa` / `fs_taau`: frame, the colour, the world depth, the history, the sampler.
pub(super) const TAA_BINDINGS: [wgpu::BindingType; 5] =
[UNIFORM, UNFILTERED, DEPTH, FLOAT, SAMPLER];
/// Group 0 of `fs_taa` / `fs_taau`: frame, colour, world depth, history, sampler, motion and G-buffer depth.
pub(super) const TAA_BINDINGS: [wgpu::BindingType; 7] =
[UNIFORM, UNFILTERED, DEPTH, FLOAT, SAMPLER, UNFILTERED, DEPTH];
/// TAA is on unless `HORIZON_HD_TAA=0`.
pub fn taa_enabled() -> bool {
@@ -29,6 +37,12 @@ pub fn taa_enabled() -> bool {
*ON.get_or_init(|| std::env::var("HORIZON_HD_TAA").map_or(true, |v| v != "0"))
}
/// Visualise opaque motion instead of resolving history (`HORIZON_HD_DEBUG_MOTION=1`).
pub fn debug_motion() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| std::env::var("HORIZON_HD_DEBUG_MOTION").is_ok_and(|v| v == "1"))
}
/// The Halton sequence's `index`-th number in `base`, in (0, 1).
fn halton(mut index: u32, base: u32) -> f32 {
let (mut f, mut r) = (1.0, 0.0);
@@ -52,10 +66,15 @@ pub fn scale_override() -> Option<f32> {
return Some(1.0);
}
static SCALE: std::sync::OnceLock<Option<f32>> = std::sync::OnceLock::new();
*SCALE.get_or_init(|| {
let configured = *SCALE.get_or_init(|| {
let v: f32 = std::env::var("HORIZON_HD_SCALE").ok()?.parse().ok()?;
v.is_finite().then(|| v.clamp(0.25, 1.0))
})
});
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
if configured.is_none() && crate::gfx::dlss::requested() {
return Some(2.0 / 3.0);
}
configured
}
static FILM: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
@@ -103,19 +122,27 @@ pub(super) struct Taa {
history_flip: usize,
/// This frame is anti-aliased (set by `write`).
pub(super) active: bool,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
pub(super) jitter: [f32; 2],
layout: wgpu::BindGroupLayout,
pipeline: wgpu::RenderPipeline,
taau_pipeline: wgpu::RenderPipeline,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
dlss: Option<dlss::Dlss>,
groups: Option<((wgpu::TextureView, wgpu::TextureView), [wgpu::BindGroup; 2])>,
}
impl Taa {
pub(super) fn new(device: &wgpu::Device) -> Self {
pub(super) fn new(
device: &wgpu::Device,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))] sdk: Option<Arc<Mutex<dlss_wgpu::DlssSdk>>>,
) -> Self {
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("HD TAA"),
entries: &entries(&TAA_BINDINGS),
});
let taa_module = module(device, "HD TAA", TAA);
let constants = [("DEBUG_MOTION", if debug_motion() { 1.0 } else { 0.0 })];
let pipeline = |entry: &str| {
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("HD TAA"),
@@ -139,7 +166,10 @@ impl Taa {
fragment: Some(wgpu::FragmentState {
module: &taa_module,
entry_point: Some(entry),
compilation_options: Default::default(),
compilation_options: wgpu::PipelineCompilationOptions {
constants: &constants,
..Default::default()
},
targets: &[target.clone(), target],
}),
primitive: Default::default(),
@@ -159,15 +189,25 @@ impl Taa {
history_ready: false,
history_flip: 0,
active: false,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
jitter: [0.0; 2],
layout,
pipeline,
taau_pipeline,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
dlss: sdk.map(|sdk| dlss::Dlss::new(sdk, device)),
groups: None,
}
}
/// New output-size targets; the history starts over.
pub(super) fn resize(&mut self, device: &wgpu::Device, out: [u32; 2]) {
pub(super) fn resize(
&mut self,
device: &wgpu::Device,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))] queue: &wgpu::Queue,
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))] world: [u32; 2],
out: [u32; 2],
) {
self.target = super::target(device, "HD TAA", out, HDR).view;
self.history = [
super::target(device, "HD TAA history", out, HDR).view,
@@ -175,6 +215,10 @@ impl Taa {
];
self.history_ready = false;
self.groups = None;
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
if let Some(dlss) = &mut self.dlss {
dlss.resize(device, queue, world, out);
}
}
}
@@ -184,10 +228,14 @@ impl Hd {
pub fn taa(
&mut self,
device: &wgpu::Device,
adapter: &wgpu::Adapter,
encoder: &mut wgpu::CommandEncoder,
submissions: &mut Vec<wgpu::CommandBuffer>,
source: HdSource,
depth: &wgpu::TextureView,
) -> HdSource {
#[cfg(not(all(feature = "dlss", any(windows, target_os = "linux"))))]
let _ = (adapter, submissions);
if !self.taa.active {
return source;
}
@@ -196,6 +244,39 @@ impl Hd {
HdSource::Fx => self.fx.clone(),
HdSource::Taa | HdSource::Film => return source,
};
#[cfg(all(feature = "dlss", any(windows, target_os = "linux")))]
if self.taa.dlss.as_ref().is_some_and(dlss::Dlss::available)
{
let mut after = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("HD DLSS output conversion"),
});
let reset = !self.taa.history_ready;
let result = self.taa.dlss.as_mut().unwrap().render(
device,
adapter,
encoder,
&mut after,
&input,
depth,
&self.gbuffer.motion,
&self.taa.target,
&self.sampler,
self.taa.jitter,
reset,
);
match result {
Ok(commands) => {
submissions.push(commands);
submissions.push(after.finish());
self.taa.history_ready = true;
return HdSource::Taa;
}
Err(e) => {
log::warn!("DLSS evaluation failed, using native TAAU: {e}");
self.taa.dlss.as_mut().unwrap().mark_failed();
}
}
}
let key = (input, depth.clone());
if self.taa.groups.as_ref().is_none_or(|(k, _)| *k != key) {
let groups = [0, 1].map(|read| {
@@ -209,6 +290,8 @@ impl Hd {
wgpu::BindingResource::TextureView(depth),
wgpu::BindingResource::TextureView(&self.taa.history[read]),
wgpu::BindingResource::Sampler(&self.sampler),
wgpu::BindingResource::TextureView(&self.gbuffer.motion),
wgpu::BindingResource::TextureView(&self.gbuffer.depth),
],
)
});
+82 -37
View File
@@ -8,6 +8,23 @@ struct TaaOut {
@group(0) @binding(2) var depth_tex: texture_depth_2d;
@group(0) @binding(3) var history: texture_2d<f32>;
@group(0) @binding(4) var linear: sampler;
@group(0) @binding(5) var motion_tex: texture_2d<f32>;
@group(0) @binding(6) var gbuffer_depth: texture_depth_2d;
// Opaque G-buffer geometry has an explicit current-to-previous vector. World-only or translucent
// pixels keep the depth/camera fallback until their own motion/reactive contract is defined.
fn history_motion(px: vec2<i32>, world_depth: f32) -> vec3<f32> {
let surface = textureLoad(gbuffer_depth, px, 0);
if surface > 0.0 && abs(surface - world_depth) <= 0.000001 {
return vec3<f32>(textureLoad(motion_tex, px, 0).xy, 1.0);
}
return vec3<f32>(0.0);
}
override DEBUG_MOTION: bool = false;
fn debug_motion(motion: vec3<f32>) -> vec4<f32> {
return vec4<f32>(abs(motion.xy) * 0.25, motion.z, 1.0);
}
fn to_ycocg(c: vec3<f32>) -> vec3<f32> {
return vec3<f32>(c.r * 0.25 + c.g * 0.5 + c.b * 0.25, c.r * 0.5 - c.b * 0.5, c.g * 0.5 - c.r * 0.25 - c.b * 0.25);
@@ -91,26 +108,37 @@ fn fs_taa(in: FullOut) -> TaaOut {
let dist = select(length(p), 0.0, sky);
var resolved = centre;
if frame.taa.w > 0.5 {
var uv = vec2<f32>(-1.0);
let prev = frame.prev_view_proj * vec4<f32>(select(p + frame.taa.xyz, p, sky), 1.0);
if prev.w > 0.0 {
let pn = prev.xy / prev.w;
let uv = vec2<f32>(pn.x * 0.5 + 0.5, 0.5 - pn.y * 0.5);
if all(uv > vec2<f32>(0.0)) && all(uv < vec2<f32>(1.0)) {
let hp = clamp(vec2<i32>(uv * vec2<f32>(size)), vec2<i32>(0), size - 1);
let seen = textureLoad(history, hp, 0).a;
let expected = select(length(p + frame.taa.xyz), 0.0, sky);
// The same surface as last frame (not uncovered), by its distance.
if abs(seen - expected) <= expected * 0.03 + 0.25 {
let mean = sum / 9.0;
let sigma = sqrt(max(sum2 / 9.0 - mean * mean, vec3<f32>(0.0)));
let box_lo = min(max(mean - sigma * 1.5, lo), centre);
let box_hi = max(min(mean + sigma * 1.5, hi), centre);
let past = clip_to_box(box_lo, box_hi, to_ycocg(squash(history_color(uv))));
// Fast motion trusts the history less.
let moved = length((uv - in.uv) * vec2<f32>(size));
let keep = mix(frame.taa_params.x, frame.taa_params.x * 0.75, clamp(moved / 4.0, 0.0, 1.0));
resolved = mix(centre, past, keep);
}
uv = vec2<f32>(pn.x * 0.5 + 0.5, 0.5 - pn.y * 0.5);
}
let motion = history_motion(px, d);
if motion.z > 0.5 {
uv = in.uv + motion.xy / vec2<f32>(size);
}
if DEBUG_MOTION {
var out: TaaOut;
out.color = debug_motion(motion);
out.history = vec4<f32>(0.0);
return out;
}
if all(uv > vec2<f32>(0.0)) && all(uv < vec2<f32>(1.0)) {
let hp = clamp(vec2<i32>(uv * vec2<f32>(size)), vec2<i32>(0), size - 1);
let seen = textureLoad(history, hp, 0).a;
let expected = select(length(p + frame.taa.xyz), 0.0, sky);
// The same surface as last frame (not uncovered), by its distance.
if abs(seen - expected) <= expected * 0.03 + 0.25 {
let mean = sum / 9.0;
let sigma = sqrt(max(sum2 / 9.0 - mean * mean, vec3<f32>(0.0)));
let box_lo = min(max(mean - sigma * 1.5, lo), centre);
let box_hi = max(min(mean + sigma * 1.5, hi), centre);
let past = clip_to_box(box_lo, box_hi, to_ycocg(squash(history_color(uv))));
// Fast motion trusts the history less.
let moved = length((uv - in.uv) * vec2<f32>(size));
let keep = mix(frame.taa_params.x, frame.taa_params.x * 0.75, clamp(moved / 4.0, 0.0, 1.0));
resolved = mix(centre, past, keep);
}
}
}
@@ -168,9 +196,15 @@ fn fs_taau(in: FullOut) -> TaaOut {
// Depth: the nearest of the 2x2 samples round the pixel (steady between frames at an edge).
let corner = vec2<i32>(floor(in.uv * low - 0.5 + jit));
var d = 0.0;
var nearest = clamp(corner, vec2<i32>(0), lowi - 1);
for (var y = 0; y <= 1; y++) {
for (var x = 0; x <= 1; x++) {
d = max(d, textureLoad(depth_tex, clamp(corner + vec2<i32>(x, y), vec2<i32>(0), lowi - 1), 0));
let sample = clamp(corner + vec2<i32>(x, y), vec2<i32>(0), lowi - 1);
let sample_depth = textureLoad(depth_tex, sample, 0);
if sample_depth > d {
d = sample_depth;
nearest = sample;
}
}
}
let sky = d <= 0.0;
@@ -182,28 +216,39 @@ fn fs_taau(in: FullOut) -> TaaOut {
let dist = select(length(p), 0.0, sky);
var resolved = current;
if frame.taa.w > 0.5 {
var uv = vec2<f32>(-1.0);
let prev = frame.prev_view_proj * vec4<f32>(select(p + frame.taa.xyz, p, sky), 1.0);
if prev.w > 0.0 {
let pn = prev.xy / prev.w;
let uv = vec2<f32>(pn.x * 0.5 + 0.5, 0.5 - pn.y * 0.5);
if all(uv > vec2<f32>(0.0)) && all(uv < vec2<f32>(1.0)) {
let hp = clamp(vec2<i32>(uv * high), vec2<i32>(0), vec2<i32>(high) - 1);
let seen = textureLoad(history, hp, 0).a;
let expected = select(length(p + frame.taa.xyz), 0.0, sky);
if abs(seen - expected) <= expected * 0.03 + 0.25 {
let mean = sum / 9.0;
let sigma = sqrt(max(sum2 / 9.0 - mean * mean, vec3<f32>(0.0)));
let box_lo = min(max(mean - sigma * 1.25, lo), current);
let box_hi = max(min(mean + sigma * 1.25, hi), current);
let past = clip_to_box(box_lo, box_hi, to_ycocg(squash(history_color(uv))));
// How far the nearest sample is from the pixel's centre (0 on it, 1 at the
// corner of the world's pixel): the farther, the less the frame says.
let near = clamp(length(t - floor(t + 0.5)) / 0.7071, 0.0, 1.0);
let fresh = (1.0 - frame.taa_params.x) * mix(1.8, 0.5, near);
let moved = length((uv - in.uv) * high);
let keep = (1.0 - fresh) * mix(1.0, 0.8, clamp(moved / 4.0, 0.0, 1.0));
resolved = mix(current, past, clamp(keep, 0.0, 0.98));
}
uv = vec2<f32>(pn.x * 0.5 + 0.5, 0.5 - pn.y * 0.5);
}
let motion = history_motion(nearest, d);
if motion.z > 0.5 {
uv = in.uv + motion.xy / low;
}
if DEBUG_MOTION {
var out: TaaOut;
out.color = debug_motion(motion);
out.history = vec4<f32>(0.0);
return out;
}
if all(uv > vec2<f32>(0.0)) && all(uv < vec2<f32>(1.0)) {
let hp = clamp(vec2<i32>(uv * high), vec2<i32>(0), vec2<i32>(high) - 1);
let seen = textureLoad(history, hp, 0).a;
let expected = select(length(p + frame.taa.xyz), 0.0, sky);
if abs(seen - expected) <= expected * 0.03 + 0.25 {
let mean = sum / 9.0;
let sigma = sqrt(max(sum2 / 9.0 - mean * mean, vec3<f32>(0.0)));
let box_lo = min(max(mean - sigma * 1.25, lo), current);
let box_hi = max(min(mean + sigma * 1.25, hi), current);
let past = clip_to_box(box_lo, box_hi, to_ycocg(squash(history_color(uv))));
// How far the nearest sample is from the pixel's centre (0 on it, 1 at the
// corner of the world's pixel): the farther, the less the frame says.
let near = clamp(length(t - floor(t + 0.5)) / 0.7071, 0.0, 1.0);
let fresh = (1.0 - frame.taa_params.x) * mix(1.8, 0.5, near);
let moved = length((uv - in.uv) * high);
let keep = (1.0 - fresh) * mix(1.0, 0.8, clamp(moved / 4.0, 0.0, 1.0));
resolved = mix(current, past, clamp(keep, 0.0, 0.98));
}
}
}
+108 -5
View File
@@ -10,19 +10,36 @@ const CHUNK: u64 = 1 << 20;
pub struct Uploads {
belt: wgpu::util::StagingBelt,
chunk: u64,
}
impl Uploads {
pub fn new(device: &wgpu::Device) -> Self {
Self::with_chunk(device, CHUNK)
}
/// A belt whose staging buffers hold `chunk` bytes (larger writes get
/// a buffer of their own).
/// A belt whose staging buffers hold `chunk` bytes (a larger write gets
/// a buffer of its own, rounded up to a power of two).
pub fn with_chunk(device: &wgpu::Device, chunk: u64) -> Self {
Self {
belt: wgpu::util::StagingBelt::new(device.clone(), chunk),
chunk,
}
}
/// A mapped staging slice of at least `size` bytes. The belt gives a
/// write larger than its chunk a buffer of exactly that size, keeps it for
/// good and only reuses it for writes that fit: an upload that grows a
/// little every frame (the entity batch while mobs appear) took a new
/// buffer each frame until the game ran out of memory (#2826). Rounded up
/// to a power of two, the large buffers it keeps are reused, one per
/// doubling.
fn allocate(&mut self, size: NonZeroU64, alignment: NonZeroU64) -> wgpu::BufferSlice<'_> {
let size = if size.get() > self.chunk {
size.checked_next_power_of_two().unwrap_or(size)
} else {
size
};
self.belt.allocate(size, alignment)
}
/// Copy tightly packed RGBA8 `pixels` (`width` by `height`) into one mip
/// level of one array layer, padding rows to the copy alignment.
pub fn texture(
@@ -41,7 +58,7 @@ impl Uploads {
};
let alignment = NonZeroU64::new(u64::from(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT))
.expect("non-zero alignment");
let slice = self.belt.allocate(size, alignment);
let slice = self.allocate(size, alignment);
{
let mut view = slice
.get_mapped_range_mut()
@@ -80,8 +97,20 @@ impl Uploads {
let Some(size) = NonZeroU64::new(bytes.len() as u64) else {
return;
};
self.belt
.write_buffer(encoder, target, offset, size)
// The belt's own `write_buffer` checks, kept now that a large write's
// slice is rounded up past them.
assert!(
offset.is_multiple_of(wgpu::COPY_BUFFER_ALIGNMENT)
&& size.get().is_multiple_of(wgpu::COPY_BUFFER_ALIGNMENT),
"buffer upload at {offset} of {size} bytes must be 4-byte aligned"
);
let alignment = NonZeroU64::new(wgpu::COPY_BUFFER_ALIGNMENT).expect("non-zero alignment");
let slice = self.allocate(size, alignment).slice(..size.get());
encoder.copy_buffer_to_buffer(slice.buffer(), slice.offset(), target, offset, size.get());
slice
.get_mapped_range_mut()
.expect("staging belt slices are mapped")
.slice(..)
.copy_from_slice(bytes);
}
/// Close this frame's staging buffers: they come back for reuse once the
@@ -217,6 +246,80 @@ mod tests {
}
}
/// Staging buffers the belt holds, once every submitted frame is done
/// with them. The belt only reports its buffers through `Debug`.
fn kept_buffers(device: &wgpu::Device, uploads: &mut Uploads) -> usize {
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
// Finishing an empty frame takes back every recalled buffer.
uploads.finish(&device.create_command_encoder(&Default::default()));
let belt = format!("{:?}", uploads.belt);
let count = |field: &str| -> usize {
let at = belt.find(&format!("{field}: ")).expect(&belt) + field.len() + 2;
let digits = belt[at..].split(|c: char| !c.is_ascii_digit()).next();
digits.and_then(|d| d.parse().ok()).expect(&belt)
};
count("active_chunks") + count("closed_chunks") + count("free_chunks")
}
/// #2826: a write larger than the chunk that grows a little every frame
/// (the entity batch while mobs appear) got a new exact-size buffer each
/// frame, all of them kept, until the game ran out of memory.
#[test]
fn growing_large_writes_reuse_the_belt_buffers() {
let Some((device, queue)) = device() else {
eprintln!("no GPU adapter: skipped");
return;
};
const CHUNK: u64 = 1 << 16;
const TARGET: usize = 4 * CHUNK as usize;
let target = device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: TARGET as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: TARGET as u64,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut uploads = Uploads::with_chunk(&device, CHUNK);
let mut expected = vec![0u8; TARGET];
let frames = 64;
for frame in 0..frames {
let mut encoder = device.create_command_encoder(&Default::default());
// One small write, then the large one, 1.5 to 3.5 chunks long.
uploads.buffer(&mut encoder, &target, TARGET as u64 - 4, &[9; 4]);
expected[TARGET - 4..].fill(9);
let len = (3 * CHUNK as usize / 2 + frame * 2048).next_multiple_of(4);
let bytes: Vec<u8> = (0..len).map(|i| (i + frame) as u8).collect();
uploads.buffer(&mut encoder, &target, 0, &bytes);
expected[..len].copy_from_slice(&bytes);
encoder.copy_buffer_to_buffer(&target, 0, &readback, 0, TARGET as u64);
uploads.finish(&encoder);
queue.submit([encoder.finish()]);
readback
.slice(..)
.map_async(wgpu::MapMode::Read, |r| r.unwrap());
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
// Only the written bytes land: the rest of the target is untouched.
assert!(
*readback.slice(..).get_mapped_range().unwrap() == expected[..],
"frame {frame}"
);
readback.unmap();
}
// A chunk for the small writes and a buffer per doubling of the large
// one (2 and 4 chunks), maybe one more where a small write took the
// start of a buffer the large one needed whole: not one per frame.
let kept = kept_buffers(&device, &mut uploads);
assert!(
kept <= 4,
"the belt kept {kept} buffers over {frames} frames"
);
}
#[test]
fn belt_buffer_writes_land_in_order_at_their_offsets() {
let Some((device, queue)) = device() else {
+30
View File
@@ -44,6 +44,36 @@ pub struct Vertex {
}
/// `Vertex::normal` of a baked-colour quad of an `entitySolid` model: the render type culls
/// back faces, which the entity pipelines here do not, so `entity.wgsl` discards them.
/// A mod-model vertex with its previous drawn world pose. Kept out of `Vertex` so ordinary
/// entities do not pay for a second position they do not have.
#[repr(C)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct MotionVertex {
pub pos: [f32; 3],
pub uv: [f32; 2],
pub layer: u32,
pub color: u32,
pub light: u32,
pub normal: [i8; 4],
pub glint_uv: [f32; 2],
pub previous: [f32; 3],
}
impl MotionVertex {
pub fn new(vertex: Vertex, previous: [f32; 3]) -> Self {
Self {
pos: vertex.pos,
uv: vertex.uv,
layer: vertex.layer,
color: vertex.color,
light: vertex.light,
normal: vertex.normal,
glint_uv: vertex.glint_uv,
previous,
}
}
}
pub const CULL_BACK: [i8; 4] = [0, 0, 0, 1];
/// A section vertex: vanilla's `DefaultVertexFormat.BLOCK` (position, colour,
/// UV0, UV2) with the sprite's array layer beside the light, in 24 bytes
+18
View File
@@ -30,6 +30,11 @@ struct Globals {
hd_wind: vec4<f32>,
// AirDef fork: the sprite layers' material classes, 4 bits each (gfx/hd/materials.rs).
hd_materials: array<vec4<u32>, 64>,
// Previous drawn (jittered) view-projection and the camera's translation since it.
prev_view_proj: mat4x4<f32>,
camera_delta: vec4<f32>,
// Render-target width and height in pixels for current-to-previous motion vectors.
hd_motion: vec4<f32>,
}
@group(0) @binding(0) var<uniform> globals: Globals;
@group(0) @binding(1) var sprites: texture_2d_array<f32>;
@@ -82,6 +87,8 @@ struct TerrainOut {
@location(7) tint: vec4<f32>,
// Sky and block light, 0 to 1.
@location(8) lm: vec2<f32>,
// The same static world point in the previous drawn camera frame.
@location(9) prev_clip: vec4<f32>,
}
// Horizon Extras' dynamic lights (dynamic_light.rs): the block light, in the vertex's units
// (16 per level), a vertex at camera-relative `pos` gets from the frame's light sources, one
@@ -189,6 +196,7 @@ fn terrain_vertex(position: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f3
let pos = position + origin + wind_offset(position + origin, uv, layer);
var out: TerrainOut;
out.clip = globals.view_proj * vec4(pos, 1.0);
out.prev_clip = globals.prev_view_proj * vec4(pos + globals.camera_delta.xyz, 1.0);
out.uv = uv;
out.layer = layer;
let block = max(f32(light.y), dynamic_block_light(pos));
@@ -227,6 +235,15 @@ struct GBufferOut {
@location(0) albedo: vec4<f32>,
@location(1) normal: vec4<f32>,
@location(2) light: vec4<f32>,
@location(3) motion: vec2<f32>,
}
fn motion_pixels(pos: vec3<f32>, previous: vec4<f32>) -> vec2<f32> {
let current = globals.view_proj * vec4(pos, 1.0);
if current.w <= 0.0 || previous.w <= 0.0 { return vec2<f32>(0.0); }
let c = current.xy / current.w;
let p = previous.xy / previous.w;
let delta = vec2<f32>((p.x - c.x) * 0.5, (c.y - p.y) * 0.5);
return delta * globals.hd_motion.xy;
}
fn face_normal(pos: vec3<f32>) -> vec3<f32> {
var n = normalize(cross(dpdx(pos), dpdy(pos)));
@@ -245,6 +262,7 @@ fn face_normal(pos: vec3<f32>) -> vec3<f32> {
out.normal = vec4(face_normal(in.pos), 1.0);
// Roughness and metalness for the sun's specular and the wet look.
out.light = vec4(clamp(in.lm, vec2(0.0), vec2(1.0)), material_params(material_class(in.layer)));
out.motion = motion_pixels(in.pos, in.prev_clip);
return out;
}
// AirDef fork, HD water: the water quads once more, after the G-buffer, into small targets of
+47
View File
@@ -44,6 +44,53 @@ pub fn open_vulkan_device(
Some(unsafe { adapter.create_device_from_hal(open, desc) })
}
/// Opens a Vulkan device and lets the caller add required extensions in the same callback.
/// Used by vendor backends that must compose with, rather than replace, HDR device creation.
#[cfg(not(target_os = "macos"))]
pub fn open_vulkan_device_with_callback(
adapter: &wgpu::Adapter,
desc: &wgpu::DeviceDescriptor<'_>,
mut register: impl FnMut(&wgpu::hal::vulkan::Adapter, &mut wgpu::hal::vulkan::CreateDeviceCallbackArgs),
) -> Option<Result<(wgpu::Device, wgpu::Queue), wgpu::RequestDeviceError>> {
use wgpu::hal::api::Vulkan;
let open = {
// Safety: the adapter and callback stay borrowed for the duration of opening the device.
let hal = unsafe { adapter.as_hal::<Vulkan>() }?;
unsafe {
hal.open_with_callback(
desc.required_features,
&desc.required_limits,
&desc.memory_hints,
Some(Box::new(|mut args| {
if instance_supports_hdr(&hal) {
args.extensions.push(ash::ext::hdr_metadata::NAME);
}
register(&hal, &mut args);
})),
)
}
.ok()?
};
// Safety: opened from this adapter with exactly `desc`'s features and the registered extensions.
Some(unsafe { adapter.create_device_from_hal(open, desc) })
}
#[cfg(not(target_os = "macos"))]
fn instance_supports_hdr(adapter: &wgpu::hal::vulkan::Adapter) -> bool {
// Safety: only extension metadata of this adapter's live instance is queried.
unsafe {
adapter
.shared_instance()
.raw_instance()
.enumerate_device_extension_properties(adapter.raw_physical_device())
.is_ok_and(|extensions| {
extensions
.iter()
.any(|e| e.extension_name_as_c_str() == Ok(ash::ext::hdr_metadata::NAME))
})
}
}
/// Tells the compositor, through `VK_EXT_hdr_metadata`, that the swapchain's
/// content is BT.709 and peaks at `peak` nits. Needs a device from
/// `open_vulkan_device` and a configured surface; does nothing otherwise.