Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed751714e0 | |||
| 7a73e8d26a | |||
| 17382445af | |||
| c962cd472f | |||
| 94b572cd24 |
Generated
+93
-15
@@ -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]]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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: &[],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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>>;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
)
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user