From 23beae96e22509574c702c602b4310f3a50c61eb Mon Sep 17 00:00:00 2001 From: Radu Ursache Date: Sun, 8 Mar 2026 12:51:33 +0200 Subject: [PATCH 1/6] fix(arch): add vulkan-headers to Arch Linux dependency list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vulkan-headers provides which is required at compile time by imgui (imgui_impl_vulkan.cpp) and vk-bootstrap. On Arch, vulkan-devel is not a package name — the headers must be installed explicitly via vulkan-headers. Also replace vulkan-devel with the correct individual packages: vulkan-headers (build-time headers) vulkan-icd-loader / vulkan-tools (runtime + utilities) Fixes build failure: fatal error: vulkan/vulkan.h: No such file or directory --- BUILD_INSTRUCTIONS.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/BUILD_INSTRUCTIONS.md b/BUILD_INSTRUCTIONS.md index 37fb3b3f..54f2041c 100644 --- a/BUILD_INSTRUCTIONS.md +++ b/BUILD_INSTRUCTIONS.md @@ -29,10 +29,14 @@ sudo apt install -y \ sudo pacman -S --needed \ base-devel cmake pkgconf git \ sdl2 glew glm openssl zlib \ - vulkan-devel vulkan-tools shaderc \ + vulkan-headers vulkan-icd-loader vulkan-tools shaderc \ ffmpeg unicorn stormlib ``` +> **Note:** `vulkan-headers` provides the `vulkan/vulkan.h` development headers required +> at build time. `vulkan-devel` is a group that includes these on some distros but is not +> available by name on Arch — install `vulkan-headers` and `vulkan-icd-loader` explicitly. + --- ## 🐧 Linux (All Distros) From 54ae05d2989964890663437654dd09e93b25501b Mon Sep 17 00:00:00 2001 From: Radu Ursache Date: Sun, 8 Mar 2026 12:51:51 +0200 Subject: [PATCH 2/6] feat: add AUR PKGBUILD for wowee-git Adds a wowee-git PKGBUILD suitable for submission to the Arch User Repository. Tracks the main branch HEAD; pkgver is auto-generated from the commit count + short hash so no manual bumping is needed on new releases. Key design decisions: - Real binaries installed to /usr/lib/wowee/ to avoid PATH clutter - /usr/bin/wowee wrapper sets WOW_DATA_PATH to the user's XDG data dir (~/.local/share/wowee/Data) so the asset path works without any user configuration - /usr/bin/wowee-extract-assets helper runs asset_extract pointed at the same XDG data dir; users run this once against their WoW client - Submodules (imgui, vk-bootstrap) fetched from local git mirrors during prepare() as required by AUR source array rules - vulkan-headers listed as makedepend (required by imgui Vulkan backend and vk-bootstrap at compile time; not needed at runtime) Note: stormlib is an AUR dependency (aur/stormlib). Users will need an AUR helper (yay, paru) to install it, or install it manually first. --- PKGBUILD | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 PKGBUILD diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 00000000..013be0a1 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,107 @@ +# Maintainer: +# Contributor: + +pkgname=wowee-git +pkgver=r.1 +pkgrel=1 +pkgdesc="Open-source World of Warcraft client with Vulkan renderer (WotLK 3.3.5a / TBC / Classic)" +arch=('x86_64') +url="https://github.com/Kelsidavis/WoWee" +license=('MIT') +depends=( + 'sdl2' + 'vulkan-icd-loader' + 'openssl' + 'zlib' + 'ffmpeg' + 'unicorn' + 'glew' + 'libx11' + 'stormlib' # AUR — required at runtime by wowee-extract-assets (libstorm.so) +) +makedepends=( + 'git' + 'cmake' + 'pkgconf' + 'glm' + 'vulkan-headers' + 'shaderc' + 'python' +) +provides=('wowee') +conflicts=('wowee') +source=("${pkgname}::git+https://github.com/Kelsidavis/WoWee.git#branch=main" + "git+https://github.com/ocornut/imgui.git" + "git+https://github.com/charles-lunarg/vk-bootstrap.git") +sha256sums=('SKIP' 'SKIP' 'SKIP') + +pkgver() { + cd "${pkgname}" + printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" +} + +prepare() { + cd "${pkgname}" + git submodule init + git config submodule.extern/imgui.url "${srcdir}/imgui" + git config submodule.extern/vk-bootstrap.url "${srcdir}/vk-bootstrap" + git -c protocol.file.allow=always submodule update +} + +build() { + cmake -S "${pkgname}" -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -Wno-dev + cmake --build build --parallel "$(nproc)" +} + +package() { + DESTDIR="${pkgdir}" cmake --install build + + # Relocate real binaries from /usr/bin → /usr/lib/wowee/ + # so wrapper scripts can live at /usr/bin instead. + install -dm755 "${pkgdir}/usr/lib/wowee" + for bin in wowee asset_extract dbc_to_csv auth_probe auth_login_probe blp_convert; do + if [[ -f "${pkgdir}/usr/bin/${bin}" ]]; then + mv "${pkgdir}/usr/bin/${bin}" "${pkgdir}/usr/lib/wowee/${bin}" + fi + done + + # Main launcher: sets WOW_DATA_PATH to the user's XDG data dir. + # The app uses WOW_DATA_PATH to locate Data/manifest.json at runtime. + install -Dm755 /dev/stdin "${pkgdir}/usr/bin/wowee" <<'EOF' +#!/bin/sh +export WOW_DATA_PATH="${XDG_DATA_HOME:-$HOME/.local/share}/wowee/Data" +exec /usr/lib/wowee/wowee "$@" +EOF + + # Asset extraction helper: runs asset_extract and outputs to the XDG data dir. + # Usage: wowee-extract-assets /path/to/WoW/Data [wotlk|tbc|classic] + install -Dm755 /dev/stdin "${pkgdir}/usr/bin/wowee-extract-assets" <<'EOF' +#!/bin/sh +if [ -z "$1" ]; then + echo "Usage: wowee-extract-assets /path/to/WoW/Data [wotlk|tbc|classic]" + exit 1 +fi +OUTPUT="${XDG_DATA_HOME:-$HOME/.local/share}/wowee/Data" +mkdir -p "${OUTPUT}" +exec /usr/lib/wowee/asset_extract --mpq-dir "$1" --output "${OUTPUT}" ${2:+--expansion "$2"} +EOF + + # License + install -Dm644 "${pkgname}/LICENSE" \ + "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" + + # Post-install instructions (shown by pacman helpers that support it) + install -Dm644 /dev/stdin \ + "${pkgdir}/usr/share/doc/${pkgname}/POST_INSTALL" <<'EOF' +==> WoWee requires game assets extracted from your own WoW client. +==> Run the following once, pointing at your WoW Data/ directory: +==> +==> wowee-extract-assets /path/to/WoW-3.3.5a/Data wotlk +==> +==> Assets are written to ~/.local/share/wowee/Data/ (or $XDG_DATA_HOME/wowee/Data/). +==> Then launch the client with: wowee +EOF +} From c3047c33baad3094f89ccf4fb068359fbc235940 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 8 Mar 2026 14:18:00 -0700 Subject: [PATCH 3/6] FSR2: fix motion vector jitter, add bicubic anti-ringing, depth-dilated MVs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Motion shader: unjitter NDC before reprojection (ndc+jitter, not ndc-jitter), compute motion against unjittered UV so static scenes produce zero motion - Pass jitter offset to motion shader (push constant 80→96 bytes) - Accumulate shader: restore Catmull-Rom bicubic with anti-ringing clamp to prevent negative-lobe halos at edges while maintaining sharpness - Add depth-dilated motion vectors (3x3 nearest-to-camera) to prevent background MVs bleeding over foreground edges - Widen neighborhood clamp gamma to 3.0, uniform 5% blend with disocclusion/velocity reactive boosting --- assets/shaders/fsr2_accumulate.comp.glsl | 89 +++++++++++++++++++++-- assets/shaders/fsr2_accumulate.comp.spv | Bin 10592 -> 18052 bytes assets/shaders/fsr2_motion.comp.glsl | 22 ++++-- assets/shaders/fsr2_motion.comp.spv | Bin 3096 -> 2184 bytes src/rendering/renderer.cpp | 15 ++-- 5 files changed, 108 insertions(+), 18 deletions(-) diff --git a/assets/shaders/fsr2_accumulate.comp.glsl b/assets/shaders/fsr2_accumulate.comp.glsl index 7fb0cb27..44e7c2ad 100644 --- a/assets/shaders/fsr2_accumulate.comp.glsl +++ b/assets/shaders/fsr2_accumulate.comp.glsl @@ -29,20 +29,84 @@ vec3 yCoCgToRgb(vec3 ycocg) { return vec3(y + co - cg, y + cg, y - co - cg); } +// Catmull-Rom bicubic (9 bilinear taps) with anti-ringing clamp. +// Sharper than bilinear; anti-ringing prevents edge halos that shift with jitter. +vec3 sampleBicubic(sampler2D tex, vec2 uv, vec2 texSize) { + vec2 invTexSize = 1.0 / texSize; + vec2 iTc = uv * texSize; + vec2 tc = floor(iTc - 0.5) + 0.5; + vec2 f = iTc - tc; + + // Catmull-Rom weights + vec2 w0 = f * (-0.5 + f * (1.0 - 0.5 * f)); + vec2 w1 = 1.0 + f * f * (-2.5 + 1.5 * f); + vec2 w2 = f * (0.5 + f * (2.0 - 1.5 * f)); + vec2 w3 = f * f * (-0.5 + 0.5 * f); + + vec2 s12 = w1 + w2; + vec2 offset12 = w2 / s12; + + vec2 tc0 = (tc - 1.0) * invTexSize; + vec2 tc3 = (tc + 2.0) * invTexSize; + vec2 tc12 = (tc + offset12) * invTexSize; + + // 3x3 bilinear taps covering 4x4 texel support + vec3 result = + (texture(tex, vec2(tc0.x, tc0.y)).rgb * w0.x + + texture(tex, vec2(tc12.x, tc0.y)).rgb * s12.x + + texture(tex, vec2(tc3.x, tc0.y)).rgb * w3.x) * w0.y + + (texture(tex, vec2(tc0.x, tc12.y)).rgb * w0.x + + texture(tex, vec2(tc12.x, tc12.y)).rgb * s12.x + + texture(tex, vec2(tc3.x, tc12.y)).rgb * w3.x) * s12.y + + (texture(tex, vec2(tc0.x, tc3.y)).rgb * w0.x + + texture(tex, vec2(tc12.x, tc3.y)).rgb * s12.x + + texture(tex, vec2(tc3.x, tc3.y)).rgb * w3.x) * w3.y; + + // Anti-ringing: clamp to range of the 4 nearest texels. + // Prevents Catmull-Rom negative lobe overshoots at high-contrast edges. + vec2 tcNear = tc * invTexSize; + vec3 t00 = texture(tex, tcNear).rgb; + vec3 t10 = texture(tex, tcNear + vec2(invTexSize.x, 0.0)).rgb; + vec3 t01 = texture(tex, tcNear + vec2(0.0, invTexSize.y)).rgb; + vec3 t11 = texture(tex, tcNear + invTexSize).rgb; + vec3 minC = min(min(t00, t10), min(t01, t11)); + vec3 maxC = max(max(t00, t10), max(t01, t11)); + return clamp(result, minC, maxC); +} + void main() { ivec2 outPixel = ivec2(gl_GlobalInvocationID.xy); ivec2 outSize = ivec2(pc.displaySize.xy); if (outPixel.x >= outSize.x || outPixel.y >= outSize.y) return; vec2 outUV = (vec2(outPixel) + 0.5) * pc.displaySize.zw; - vec3 currentColor = texture(sceneColor, outUV).rgb; + + // Bicubic upsampling with anti-ringing: sharp without edge halos + vec3 currentColor = sampleBicubic(sceneColor, outUV, pc.internalSize.xy); if (pc.params.x > 0.5) { imageStore(historyOutput, outPixel, vec4(currentColor, 1.0)); return; } - vec2 motion = texture(motionVectors, outUV).rg; + // Depth-dilated motion vector: pick the MV from the nearest-to-camera + // pixel in a 3x3 neighborhood. Prevents background MVs from bleeding + // over foreground edges. + vec2 texelSize = pc.internalSize.zw; + float closestDepth = texture(depthBuffer, outUV).r; + vec2 closestOffset = vec2(0.0); + for (int y = -1; y <= 1; y++) { + for (int x = -1; x <= 1; x++) { + vec2 off = vec2(float(x), float(y)) * texelSize; + float d = texture(depthBuffer, outUV + off).r; + if (d < closestDepth) { + closestDepth = d; + closestOffset = off; + } + } + } + vec2 motion = texture(motionVectors, outUV + closestOffset).rg; + vec2 historyUV = outUV + motion; float historyValid = (historyUV.x >= 0.0 && historyUV.x <= 1.0 && @@ -50,8 +114,9 @@ void main() { vec3 historyColor = texture(historyInput, historyUV).rgb; - // Neighborhood clamping in YCoCg space - vec2 texelSize = pc.internalSize.zw; + // Neighborhood clamping in YCoCg space with wide gamma. + // Wide gamma (3.0) prevents jitter-chasing: the clamp box only catches + // truly stale history (disocclusion), not normal jitter variation. vec3 s0 = rgbToYCoCg(currentColor); vec3 s1 = rgbToYCoCg(texture(sceneColor, outUV + vec2(-texelSize.x, 0.0)).rgb); vec3 s2 = rgbToYCoCg(texture(sceneColor, outUV + vec2( texelSize.x, 0.0)).rgb); @@ -68,7 +133,7 @@ void main() { vec3 variance = max(m2 / 9.0 - mean * mean, vec3(0.0)); vec3 stddev = sqrt(variance); - float gamma = 1.5; + float gamma = 3.0; vec3 boxMin = mean - gamma * stddev; vec3 boxMax = mean + gamma * stddev; @@ -77,7 +142,19 @@ void main() { historyColor = yCoCgToRgb(clampedHistory); float clampDist = length(historyYCoCg - clampedHistory); - float blendFactor = mix(0.05, 0.30, clamp(clampDist * 2.0, 0.0, 1.0)); + + // Uniform 5% blend: ~45 frames for 90% convergence. + // Simpler than edge-aware; the anti-ringing bicubic handles edge stability. + float blendFactor = 0.05; + + // Disocclusion: large clamp distance → rapidly replace stale history + blendFactor = mix(blendFactor, 0.60, clamp(clampDist * 5.0, 0.0, 1.0)); + + // Velocity: higher blend during motion reduces ghosting + float motionMag = length(motion * pc.displaySize.xy); + blendFactor = max(blendFactor, clamp(motionMag * 0.15, 0.0, 0.35)); + + // Full current frame when history is out of bounds blendFactor = mix(blendFactor, 1.0, 1.0 - historyValid); vec3 result = mix(historyColor, currentColor, blendFactor); diff --git a/assets/shaders/fsr2_accumulate.comp.spv b/assets/shaders/fsr2_accumulate.comp.spv index 47529d75ae3b2f227713851a616bea13ce070b00..f69e0f3b26e197e6bb3f1616935effe61fa9b971 100644 GIT binary patch literal 18052 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 literal 10592 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 imgSize.x || pixelCoord.y >= imgSize.y) return; - // Sample depth (Vulkan: 0 = near, 1 = far) float depth = texelFetch(depthBuffer, pixelCoord, 0).r; - // Pixel center in UV [0,1] and NDC [-1,1] + // Pixel center UV and NDC vec2 uv = (vec2(pixelCoord) + 0.5) * pc.resolution.zw; vec2 ndc = uv * 2.0 - 1.0; - // Clip-to-clip reprojection: current unjittered clip → previous unjittered clip - vec4 clipPos = vec4(ndc, depth, 1.0); + // Unjitter the NDC: the scene was rendered with jitter applied to + // projection[2][0/1]. For RH perspective (P[2][3]=-1, clip.w=-vz): + // jittered_ndc = unjittered_ndc - jitter + // unjittered_ndc = ndc + jitter + vec2 unjitteredNDC = ndc + pc.jitterOffset.xy; + + // Reproject to previous frame via unjittered VP matrices + vec4 clipPos = vec4(unjitteredNDC, depth, 1.0); vec4 prevClip = pc.reprojMatrix * clipPos; vec2 prevNdc = prevClip.xy / prevClip.w; vec2 prevUV = prevNdc * 0.5 + 0.5; - // Motion = previous position - current position (both unjittered, in UV space) - vec2 motion = prevUV - uv; + // Current unjittered UV for this pixel's world content + vec2 currentUnjitteredUV = unjitteredNDC * 0.5 + 0.5; + + // Motion between unjittered positions — jitter-free. + // For a static scene (identity reprojMatrix), this is exactly zero. + vec2 motion = prevUV - currentUnjitteredUV; imageStore(motionVectors, pixelCoord, vec4(motion, 0.0, 0.0)); } diff --git a/assets/shaders/fsr2_motion.comp.spv b/assets/shaders/fsr2_motion.comp.spv index faa3d8362634407aa4109d6e924888330bdb0a92..8a50542d0f9a23e5845c8ef189f505ef3763d5be 100644 GIT binary patch literal 2184 zcmZvbTT50^5QcYJb}&zP&a%&pPMT>)OH6}8DM>3j-K41mLKSw+zv8M`so zv-o7T=j5qgBdfD=MbTG7%og;`VE+Exf_xRIg_o-X+Y-z@0;|6g>;|LQO-Pq{M)`)_ zS!Dm^Xqw!AIdd8}T*8$lZUQ+%ujZ4p_BrG};0n7VH;eW%dZ}Mqf8Kj}rriqMyN>i6 z+VxbhMhEr09rZ?HDv$->{$_0Zmka+6^a60{H=mq23fMc5?v)=%w)YqLs?m)%UlY6P zkp*C`2Bfy{(89M7DW~0@?m0Ml>op^{1Mkdw+AWFe!nSwM8})agTf_S9Z$rA@d=0P# zr2Fi#1GvKOLN`wTRRIM+0~=kmVncM18wevS8y?ePWDWvo4FJ3p?dxtg0r+dFy#{9n4c zY2Vx#lM~)oH}B@1e*m6G1osi$GlXwm8Q0u`JA|83{SSuvmUum{MS&oi_qPTpm{g_LhEy8dhA z^DXuRYq)|tfNq@Y3vmVHL13)D&*Ul~4*_}4eFW+HiBJA8kn`;7k^3mR_3SCQW9ZhC zi+a82#_Ib)j>x6_$~gk<2Xhl{}2m&9m~B+eO&x4Mk(I|*|-Pirau3c7oY zd6&3xbb0@=)x=##mv>HMPHv$)C;n?OC%4h%qxKziIpOmDqu0CW<~1hHcn@9P72G7c zoO>tIJx!q-t8W-g0Q;Io&H(%J?(EC|eiq2TC&qhzkhm#i^g5S#eJ<-o4-e7hy}OBY zC-dm?Vm)yS=;qNE_xTuI-v2$$zKAaGJ4M`6bkA8D_Y7O!xOj`_=QWe muh5Ni1-FJSS9*`Hv5nQYnlMAo6?ErHf9Whc)4$2k5B>lsn|<8? literal 3096 zcmZvdYj0Fl6oyYPv_J(!C>K$jf_D%>1yO{SmO4}mEpkzdIxRDYa&S6ln3>jsm`IF? zpG^E0e)B*0pNyBp#OFD)H;#>)yj|;k_u6Z%eL2HG|KZ^z=}ShEZOJ!DckE31QIbS^ z0y~ln6}&uqb@o`hQ#TATkydJ1E3J02wbZ!HJ#ks1T3@a825cgtxBezN z+Cq(Hww~6f^1M|`P&OBRE!QjMdVXuEKGRsst4p0MZ_HdE)*Me^vz6tm*@F~5l++$n#(lQZc3xjKy}1XHeQ3>Ux6Ud2 zTH5T~!P@J)kx#DPzMZy;_(`ubT(Vbs`ujH|}p~QW4`3;UdZ^_Hl@*YRPGvFfJ^Kf|I7x^3c;X?hd#^d4* z{Hu8;#!Y}S=iyr4fpy&fA>fF7tV;`SKfUvE=gny@`ToKmOdewGS-y)3))*tk7pp%? zy&-G~xPC9(Ue%m0!F{i4!uYL)&L4*H=2tVP@lRmQrG6%G&o1WQj&HpA_poXgwgk+z z8!Pwy?&I$$R!x48zn)XnthE>J+g7)h+_$B61nxfEd#wF5zWL+c4&WPWjoqjvtn2J= z3^=TF2pi8O&L2TDuk$Cvyt((2@olWW^SvYT1MK@W6UMpzoe-qvKIFawN8HERE-m8E zao$?ffH5BVvxf72+o!=l?Dyvx diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 063bae9a..c3449f93 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -3785,7 +3785,7 @@ bool Renderer::initFSR2Resources() { VkPushConstantRange pc{}; pc.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; pc.offset = 0; - pc.size = sizeof(glm::mat4) + sizeof(glm::vec4); // 80 bytes + pc.size = sizeof(glm::mat4) + 2 * sizeof(glm::vec4); // 96 bytes VkPipelineLayoutCreateInfo plCI{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; plCI.setLayoutCount = 1; @@ -4086,17 +4086,20 @@ void Renderer::dispatchMotionVectors() { vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_COMPUTE, fsr2_.motionVecPipelineLayout, 0, 1, &fsr2_.motionVecDescSet, 0, nullptr); - // Single reprojection matrix: prevUnjitteredVP * inv(currentUnjitteredVP) - // Both matrices are unjittered — jitter only affects sub-pixel sampling, - // not motion vector computation. This avoids numerical instability from - // jitter amplification through large world coordinates. + // Reprojection: prevUnjitteredVP * inv(currentUnjitteredVP) + // Using unjittered VPs avoids numerical instability from jitter amplification + // through large world coordinates. The shader corrects NDC by subtracting + // current jitter before reprojection (depth was rendered at jittered position). struct { - glm::mat4 reprojMatrix; // prevUnjitteredVP * inv(currentUnjitteredVP) + glm::mat4 reprojMatrix; glm::vec4 resolution; + glm::vec4 jitterOffset; // xy = current jitter (NDC), zw = unused } pc; glm::mat4 currentUnjitteredVP = camera->getUnjitteredViewProjectionMatrix(); pc.reprojMatrix = fsr2_.prevViewProjection * glm::inverse(currentUnjitteredVP); + glm::vec2 jitter = camera->getJitter(); + pc.jitterOffset = glm::vec4(jitter.x, jitter.y, 0.0f, 0.0f); pc.resolution = glm::vec4( static_cast(fsr2_.internalWidth), static_cast(fsr2_.internalHeight), From f74dcc37e09ba6af0d1476fc92b775fc9c3a1514 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 8 Mar 2026 14:34:58 -0700 Subject: [PATCH 4/6] FSR2: reduce doubling via tighter clamp, MV dead zone, luminance stability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Motion shader: zero out sub-0.01px motion to eliminate float precision noise in reprojection (distant geometry with large world coords) - Accumulate: tighten neighborhood clamp gamma 3.0→1.5 to catch slightly misaligned history causing ghost doubles - Reduce max jitter-aware blend 30%→20% for less visible oscillation - Add luminance instability dampening: reduce blend when current frame disagrees with history to prevent shimmer on small/distant features --- assets/shaders/fsr2_accumulate.comp.glsl | 30 ++++++++++++++++++++--- assets/shaders/fsr2_accumulate.comp.spv | Bin 18052 -> 19720 bytes assets/shaders/fsr2_motion.comp.glsl | 7 ++++++ assets/shaders/fsr2_motion.comp.spv | Bin 2184 -> 3728 bytes 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/assets/shaders/fsr2_accumulate.comp.glsl b/assets/shaders/fsr2_accumulate.comp.glsl index 44e7c2ad..9684b7c3 100644 --- a/assets/shaders/fsr2_accumulate.comp.glsl +++ b/assets/shaders/fsr2_accumulate.comp.glsl @@ -133,7 +133,10 @@ void main() { vec3 variance = max(m2 / 9.0 - mean * mean, vec3(0.0)); vec3 stddev = sqrt(variance); - float gamma = 3.0; + // Tighter clamp (gamma 1.5) catches slightly misaligned history that + // causes doubling. With jitter-aware blending providing stability, + // the clamp can be tight without causing jitter-chasing. + float gamma = 1.5; vec3 boxMin = mean - gamma * stddev; vec3 boxMax = mean + gamma * stddev; @@ -143,9 +146,28 @@ void main() { float clampDist = length(historyYCoCg - clampedHistory); - // Uniform 5% blend: ~45 frames for 90% convergence. - // Simpler than edge-aware; the anti-ringing bicubic handles edge stability. - float blendFactor = 0.05; + // Jitter-aware sample weighting: compute how close the current frame's + // jittered sample fell to this output pixel. Close samples are high quality + // (blend aggressively for fast convergence), distant samples are low quality + // (blend minimally to avoid visible jitter). + vec2 jitterPx = pc.jitterOffset.xy * 0.5 * pc.internalSize.xy; + vec2 internalPos = outUV * pc.internalSize.xy; + vec2 subPixelOffset = fract(internalPos) - 0.5; + vec2 sampleDelta = subPixelOffset - jitterPx; + float dist2 = dot(sampleDelta, sampleDelta); + float sampleQuality = exp(-dist2 * 3.0); + float baseBlend = mix(0.02, 0.20, sampleQuality); + + // Luminance instability: when current frame differs significantly from + // history, it may be aliased/flickering content. Reduce blend to prevent + // oscillation, especially for small distant features. + float lumCurrent = dot(currentColor, vec3(0.299, 0.587, 0.114)); + float lumHistory = dot(historyColor, vec3(0.299, 0.587, 0.114)); + float lumDelta = abs(lumCurrent - lumHistory) / max(max(lumCurrent, lumHistory), 0.01); + float stability = 1.0 - clamp(lumDelta * 3.0, 0.0, 0.7); + baseBlend *= stability; + + float blendFactor = baseBlend; // Disocclusion: large clamp distance → rapidly replace stale history blendFactor = mix(blendFactor, 0.60, clamp(clampDist * 5.0, 0.0, 1.0)); diff --git a/assets/shaders/fsr2_accumulate.comp.spv b/assets/shaders/fsr2_accumulate.comp.spv index f69e0f3b26e197e6bb3f1616935effe61fa9b971..640fab16dcae1df79e369f65c1959a9d2e37e5ff 100644 GIT binary patch delta 3334 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 KddS}_(Sbi0Bwif= From 2003cc8aaa31b4b64463f9129853f775bbd53741 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 8 Mar 2026 14:50:14 -0700 Subject: [PATCH 5/6] FSR2: de-jitter scene sampling, fix loading screen progress FSR2 temporal upscaling: - De-jitter scene color sampling (outUV - jitterUV) for frame-to-frame consistency, eliminating the primary source of temporal jitter - Remove luminance instability dampening (was causing excessive blur) - Simplify to uniform 8% blend (de-jittered values are consistent) - Gamma 2.0 for moderate neighborhood clamping - Motion vector dead zone: zero sub-0.01px motion from float precision noise Loading screen: - Reduce tile load radius from 3 to 2 (25 tiles) for faster loading - Process one tile per iteration for smooth progress bar updates --- assets/shaders/fsr2_accumulate.comp.glsl | 64 +++++++++-------------- assets/shaders/fsr2_accumulate.comp.spv | Bin 19720 -> 18288 bytes src/core/application.cpp | 10 ++-- 3 files changed, 29 insertions(+), 45 deletions(-) diff --git a/assets/shaders/fsr2_accumulate.comp.glsl b/assets/shaders/fsr2_accumulate.comp.glsl index 9684b7c3..756945f0 100644 --- a/assets/shaders/fsr2_accumulate.comp.glsl +++ b/assets/shaders/fsr2_accumulate.comp.glsl @@ -81,8 +81,15 @@ void main() { vec2 outUV = (vec2(outPixel) + 0.5) * pc.displaySize.zw; - // Bicubic upsampling with anti-ringing: sharp without edge halos - vec3 currentColor = sampleBicubic(sceneColor, outUV, pc.internalSize.xy); + // De-jitter: the scene was rendered with sub-pixel jitter, effectively + // shifting the internal image by jitterUV. Sampling at (outUV - jitterUV) + // undoes this shift, reconstructing the scene at the output pixel's true + // unjittered position. This makes the sampled value consistent across + // frames, eliminating the primary source of temporal jitter. + vec2 jitterUV = pc.jitterOffset.xy * 0.5; + vec2 dejitteredUV = outUV - jitterUV; + + vec3 currentColor = sampleBicubic(sceneColor, dejitteredUV, pc.internalSize.xy); if (pc.params.x > 0.5) { imageStore(historyOutput, outPixel, vec4(currentColor, 1.0)); @@ -114,18 +121,18 @@ void main() { vec3 historyColor = texture(historyInput, historyUV).rgb; - // Neighborhood clamping in YCoCg space with wide gamma. - // Wide gamma (3.0) prevents jitter-chasing: the clamp box only catches - // truly stale history (disocclusion), not normal jitter variation. + // Neighborhood clamping in YCoCg space at de-jittered positions. + // De-jittered neighborhood is stable across frames, preventing + // the clamp box from chasing jitter. vec3 s0 = rgbToYCoCg(currentColor); - vec3 s1 = rgbToYCoCg(texture(sceneColor, outUV + vec2(-texelSize.x, 0.0)).rgb); - vec3 s2 = rgbToYCoCg(texture(sceneColor, outUV + vec2( texelSize.x, 0.0)).rgb); - vec3 s3 = rgbToYCoCg(texture(sceneColor, outUV + vec2(0.0, -texelSize.y)).rgb); - vec3 s4 = rgbToYCoCg(texture(sceneColor, outUV + vec2(0.0, texelSize.y)).rgb); - vec3 s5 = rgbToYCoCg(texture(sceneColor, outUV + vec2(-texelSize.x, -texelSize.y)).rgb); - vec3 s6 = rgbToYCoCg(texture(sceneColor, outUV + vec2( texelSize.x, -texelSize.y)).rgb); - vec3 s7 = rgbToYCoCg(texture(sceneColor, outUV + vec2(-texelSize.x, texelSize.y)).rgb); - vec3 s8 = rgbToYCoCg(texture(sceneColor, outUV + vec2( texelSize.x, texelSize.y)).rgb); + vec3 s1 = rgbToYCoCg(texture(sceneColor, dejitteredUV + vec2(-texelSize.x, 0.0)).rgb); + vec3 s2 = rgbToYCoCg(texture(sceneColor, dejitteredUV + vec2( texelSize.x, 0.0)).rgb); + vec3 s3 = rgbToYCoCg(texture(sceneColor, dejitteredUV + vec2(0.0, -texelSize.y)).rgb); + vec3 s4 = rgbToYCoCg(texture(sceneColor, dejitteredUV + vec2(0.0, texelSize.y)).rgb); + vec3 s5 = rgbToYCoCg(texture(sceneColor, dejitteredUV + vec2(-texelSize.x, -texelSize.y)).rgb); + vec3 s6 = rgbToYCoCg(texture(sceneColor, dejitteredUV + vec2( texelSize.x, -texelSize.y)).rgb); + vec3 s7 = rgbToYCoCg(texture(sceneColor, dejitteredUV + vec2(-texelSize.x, texelSize.y)).rgb); + vec3 s8 = rgbToYCoCg(texture(sceneColor, dejitteredUV + vec2( texelSize.x, texelSize.y)).rgb); vec3 m1 = s0 + s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8; vec3 m2 = s0*s0 + s1*s1 + s2*s2 + s3*s3 + s4*s4 + s5*s5 + s6*s6 + s7*s7 + s8*s8; @@ -133,10 +140,7 @@ void main() { vec3 variance = max(m2 / 9.0 - mean * mean, vec3(0.0)); vec3 stddev = sqrt(variance); - // Tighter clamp (gamma 1.5) catches slightly misaligned history that - // causes doubling. With jitter-aware blending providing stability, - // the clamp can be tight without causing jitter-chasing. - float gamma = 1.5; + float gamma = 2.0; vec3 boxMin = mean - gamma * stddev; vec3 boxMax = mean + gamma * stddev; @@ -146,28 +150,10 @@ void main() { float clampDist = length(historyYCoCg - clampedHistory); - // Jitter-aware sample weighting: compute how close the current frame's - // jittered sample fell to this output pixel. Close samples are high quality - // (blend aggressively for fast convergence), distant samples are low quality - // (blend minimally to avoid visible jitter). - vec2 jitterPx = pc.jitterOffset.xy * 0.5 * pc.internalSize.xy; - vec2 internalPos = outUV * pc.internalSize.xy; - vec2 subPixelOffset = fract(internalPos) - 0.5; - vec2 sampleDelta = subPixelOffset - jitterPx; - float dist2 = dot(sampleDelta, sampleDelta); - float sampleQuality = exp(-dist2 * 3.0); - float baseBlend = mix(0.02, 0.20, sampleQuality); - - // Luminance instability: when current frame differs significantly from - // history, it may be aliased/flickering content. Reduce blend to prevent - // oscillation, especially for small distant features. - float lumCurrent = dot(currentColor, vec3(0.299, 0.587, 0.114)); - float lumHistory = dot(historyColor, vec3(0.299, 0.587, 0.114)); - float lumDelta = abs(lumCurrent - lumHistory) / max(max(lumCurrent, lumHistory), 0.01); - float stability = 1.0 - clamp(lumDelta * 3.0, 0.0, 0.7); - baseBlend *= stability; - - float blendFactor = baseBlend; + // With de-jittered sampling, the reconstructed value is consistent across + // frames, so a uniform blend rate works without causing visible jitter. + // 8% gives ~28 frames for 90% convergence (~0.5s at 60fps). + float blendFactor = 0.08; // Disocclusion: large clamp distance → rapidly replace stale history blendFactor = mix(blendFactor, 0.60, clamp(clampDist * 5.0, 0.0, 1.0)); diff --git a/assets/shaders/fsr2_accumulate.comp.spv b/assets/shaders/fsr2_accumulate.comp.spv index 640fab16dcae1df79e369f65c1959a9d2e37e5ff..2ea3acb515e67eced0532dde96d59d0168498a15 100644 GIT binary patch literal 18288 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 diff --git a/src/core/application.cpp b/src/core/application.cpp index f9ac557c..d6ae3065 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -4042,7 +4042,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float // then restore the full radius after entering the game. // This matches WoW's behavior: load quickly, stream the rest in-game. const int savedLoadRadius = 4; - terrainMgr->setLoadRadius(1); + terrainMgr->setLoadRadius(2); // 5x5=25 tiles — balance between spawn hitches and load time terrainMgr->setUnloadRadius(7); // Trigger tile streaming for surrounding area @@ -4080,11 +4080,9 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float // Trigger new streaming — enqueue tiles for background workers terrainMgr->update(*camera, 0.016f); - // Process ALL available ready tiles per iteration — batches GPU - // uploads into a single command buffer + fence wait instead of - // one fence per tile. Loading screen still updates between - // iterations while workers parse more tiles. - terrainMgr->processAllReadyTiles(); + // Process ONE tile per iteration so the progress bar updates + // smoothly between tiles instead of stalling on large batches. + terrainMgr->processOneReadyTile(); int remaining = terrainMgr->getRemainingTileCount(); int loaded = terrainMgr->getLoadedTileCount(); From a8500a80b5e30b8703899a7a00b23e04c3a12f55 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 8 Mar 2026 15:15:44 -0700 Subject: [PATCH 6/6] FSR2: selective clamp, tonemapped accumulation, terrain load radius 3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Selective neighborhood clamp: only modify history when there's actual motion or disocclusion — static pixels pass history through untouched, preventing jitter-chasing from the shifting variance box - Tonemapped accumulation: Reinhard tonemap before blend compresses bright edges so they don't disproportionately cause jitter - Jitter-aware sample weighting: blend 3-20% based on sample proximity - Soft MV dead zone: smoothstep instead of step avoids spatial discontinuity - Aggressive velocity response: 30%/px motion, 50% cap, 80% disocclusion - Terrain loading: radius 3 (49 tiles) to prevent spawn hitches, processOneReadyTile for smooth progress bar updates --- assets/shaders/fsr2_accumulate.comp.glsl | 107 +++++++++++++---------- assets/shaders/fsr2_accumulate.comp.spv | Bin 18288 -> 20832 bytes assets/shaders/fsr2_motion.comp.glsl | 8 +- assets/shaders/fsr2_motion.comp.spv | Bin 3728 -> 3732 bytes src/core/application.cpp | 2 +- 5 files changed, 65 insertions(+), 52 deletions(-) diff --git a/assets/shaders/fsr2_accumulate.comp.glsl b/assets/shaders/fsr2_accumulate.comp.glsl index 756945f0..bcaad6f8 100644 --- a/assets/shaders/fsr2_accumulate.comp.glsl +++ b/assets/shaders/fsr2_accumulate.comp.glsl @@ -15,6 +15,16 @@ layout(push_constant) uniform PushConstants { vec4 params; // x = resetHistory (1=reset), y = sharpness, zw = unused } pc; +vec3 tonemap(vec3 c) { + float luma = max(dot(c, vec3(0.299, 0.587, 0.114)), 0.0); + return c / (1.0 + luma); +} + +vec3 inverseTonemap(vec3 c) { + float luma = max(dot(c, vec3(0.299, 0.587, 0.114)), 0.0); + return c / max(1.0 - luma, 1e-4); +} + vec3 rgbToYCoCg(vec3 rgb) { float y = 0.25 * rgb.r + 0.5 * rgb.g + 0.25 * rgb.b; float co = 0.5 * rgb.r - 0.5 * rgb.b; @@ -30,14 +40,12 @@ vec3 yCoCgToRgb(vec3 ycocg) { } // Catmull-Rom bicubic (9 bilinear taps) with anti-ringing clamp. -// Sharper than bilinear; anti-ringing prevents edge halos that shift with jitter. vec3 sampleBicubic(sampler2D tex, vec2 uv, vec2 texSize) { vec2 invTexSize = 1.0 / texSize; vec2 iTc = uv * texSize; vec2 tc = floor(iTc - 0.5) + 0.5; vec2 f = iTc - tc; - // Catmull-Rom weights vec2 w0 = f * (-0.5 + f * (1.0 - 0.5 * f)); vec2 w1 = 1.0 + f * f * (-2.5 + 1.5 * f); vec2 w2 = f * (0.5 + f * (2.0 - 1.5 * f)); @@ -50,7 +58,6 @@ vec3 sampleBicubic(sampler2D tex, vec2 uv, vec2 texSize) { vec2 tc3 = (tc + 2.0) * invTexSize; vec2 tc12 = (tc + offset12) * invTexSize; - // 3x3 bilinear taps covering 4x4 texel support vec3 result = (texture(tex, vec2(tc0.x, tc0.y)).rgb * w0.x + texture(tex, vec2(tc12.x, tc0.y)).rgb * s12.x + @@ -62,8 +69,7 @@ vec3 sampleBicubic(sampler2D tex, vec2 uv, vec2 texSize) { texture(tex, vec2(tc12.x, tc3.y)).rgb * s12.x + texture(tex, vec2(tc3.x, tc3.y)).rgb * w3.x) * w3.y; - // Anti-ringing: clamp to range of the 4 nearest texels. - // Prevents Catmull-Rom negative lobe overshoots at high-contrast edges. + // Anti-ringing: clamp to range of the 4 nearest texels vec2 tcNear = tc * invTexSize; vec3 t00 = texture(tex, tcNear).rgb; vec3 t10 = texture(tex, tcNear + vec2(invTexSize.x, 0.0)).rgb; @@ -81,24 +87,14 @@ void main() { vec2 outUV = (vec2(outPixel) + 0.5) * pc.displaySize.zw; - // De-jitter: the scene was rendered with sub-pixel jitter, effectively - // shifting the internal image by jitterUV. Sampling at (outUV - jitterUV) - // undoes this shift, reconstructing the scene at the output pixel's true - // unjittered position. This makes the sampled value consistent across - // frames, eliminating the primary source of temporal jitter. - vec2 jitterUV = pc.jitterOffset.xy * 0.5; - vec2 dejitteredUV = outUV - jitterUV; - - vec3 currentColor = sampleBicubic(sceneColor, dejitteredUV, pc.internalSize.xy); + vec3 currentColor = sampleBicubic(sceneColor, outUV, pc.internalSize.xy); if (pc.params.x > 0.5) { imageStore(historyOutput, outPixel, vec4(currentColor, 1.0)); return; } - // Depth-dilated motion vector: pick the MV from the nearest-to-camera - // pixel in a 3x3 neighborhood. Prevents background MVs from bleeding - // over foreground edges. + // Depth-dilated motion vector (3x3 nearest-to-camera) vec2 texelSize = pc.internalSize.zw; float closestDepth = texture(depthBuffer, outUV).r; vec2 closestOffset = vec2(0.0); @@ -113,26 +109,27 @@ void main() { } } vec2 motion = texture(motionVectors, outUV + closestOffset).rg; + float motionMag = length(motion * pc.displaySize.xy); vec2 historyUV = outUV + motion; - float historyValid = (historyUV.x >= 0.0 && historyUV.x <= 1.0 && historyUV.y >= 0.0 && historyUV.y <= 1.0) ? 1.0 : 0.0; - vec3 historyColor = texture(historyInput, historyUV).rgb; - // Neighborhood clamping in YCoCg space at de-jittered positions. - // De-jittered neighborhood is stable across frames, preventing - // the clamp box from chasing jitter. - vec3 s0 = rgbToYCoCg(currentColor); - vec3 s1 = rgbToYCoCg(texture(sceneColor, dejitteredUV + vec2(-texelSize.x, 0.0)).rgb); - vec3 s2 = rgbToYCoCg(texture(sceneColor, dejitteredUV + vec2( texelSize.x, 0.0)).rgb); - vec3 s3 = rgbToYCoCg(texture(sceneColor, dejitteredUV + vec2(0.0, -texelSize.y)).rgb); - vec3 s4 = rgbToYCoCg(texture(sceneColor, dejitteredUV + vec2(0.0, texelSize.y)).rgb); - vec3 s5 = rgbToYCoCg(texture(sceneColor, dejitteredUV + vec2(-texelSize.x, -texelSize.y)).rgb); - vec3 s6 = rgbToYCoCg(texture(sceneColor, dejitteredUV + vec2( texelSize.x, -texelSize.y)).rgb); - vec3 s7 = rgbToYCoCg(texture(sceneColor, dejitteredUV + vec2(-texelSize.x, texelSize.y)).rgb); - vec3 s8 = rgbToYCoCg(texture(sceneColor, dejitteredUV + vec2( texelSize.x, texelSize.y)).rgb); + // === Tonemapped accumulation === + vec3 tmCurrent = tonemap(currentColor); + vec3 tmHistory = tonemap(historyColor); + + // Neighborhood in tonemapped YCoCg + vec3 s0 = rgbToYCoCg(tmCurrent); + vec3 s1 = rgbToYCoCg(tonemap(texture(sceneColor, outUV + vec2(-texelSize.x, 0.0)).rgb)); + vec3 s2 = rgbToYCoCg(tonemap(texture(sceneColor, outUV + vec2( texelSize.x, 0.0)).rgb)); + vec3 s3 = rgbToYCoCg(tonemap(texture(sceneColor, outUV + vec2(0.0, -texelSize.y)).rgb)); + vec3 s4 = rgbToYCoCg(tonemap(texture(sceneColor, outUV + vec2(0.0, texelSize.y)).rgb)); + vec3 s5 = rgbToYCoCg(tonemap(texture(sceneColor, outUV + vec2(-texelSize.x, -texelSize.y)).rgb)); + vec3 s6 = rgbToYCoCg(tonemap(texture(sceneColor, outUV + vec2( texelSize.x, -texelSize.y)).rgb)); + vec3 s7 = rgbToYCoCg(tonemap(texture(sceneColor, outUV + vec2(-texelSize.x, texelSize.y)).rgb)); + vec3 s8 = rgbToYCoCg(tonemap(texture(sceneColor, outUV + vec2( texelSize.x, texelSize.y)).rgb)); vec3 m1 = s0 + s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8; vec3 m2 = s0*s0 + s1*s1 + s2*s2 + s3*s3 + s4*s4 + s5*s5 + s6*s6 + s7*s7 + s8*s8; @@ -140,31 +137,49 @@ void main() { vec3 variance = max(m2 / 9.0 - mean * mean, vec3(0.0)); vec3 stddev = sqrt(variance); - float gamma = 2.0; + float gamma = 1.5; vec3 boxMin = mean - gamma * stddev; vec3 boxMax = mean + gamma * stddev; - vec3 historyYCoCg = rgbToYCoCg(historyColor); - vec3 clampedHistory = clamp(historyYCoCg, boxMin, boxMax); - historyColor = yCoCgToRgb(clampedHistory); + // Compute clamped history and measure how far it was from the box + vec3 tmHistYCoCg = rgbToYCoCg(tmHistory); + vec3 clampedYCoCg = clamp(tmHistYCoCg, boxMin, boxMax); + float clampDist = length(tmHistYCoCg - clampedYCoCg); - float clampDist = length(historyYCoCg - clampedHistory); + // SELECTIVE CLAMP: only modify history when there's motion or disocclusion. + // For static pixels, history is already well-accumulated — clamping it + // each frame causes the clamp box (which shifts with jitter) to drag + // the history around, creating visible shimmer. By leaving static history + // untouched, accumulated anti-aliasing and detail is preserved. + float needsClamp = max( + clamp(motionMag * 2.0, 0.0, 1.0), // motion → full clamp + clamp(clampDist * 3.0, 0.0, 1.0) // disocclusion → full clamp + ); + tmHistory = yCoCgToRgb(mix(tmHistYCoCg, clampedYCoCg, needsClamp)); - // With de-jittered sampling, the reconstructed value is consistent across - // frames, so a uniform blend rate works without causing visible jitter. - // 8% gives ~28 frames for 90% convergence (~0.5s at 60fps). - float blendFactor = 0.08; + // Blend: higher for good jitter samples, lower for poor ones. + // Jitter-aware weighting: current frame's sample quality depends on + // how close the jittered sample fell to this output pixel. + vec2 jitterPx = pc.jitterOffset.xy * 0.5 * pc.internalSize.xy; + vec2 internalPos = outUV * pc.internalSize.xy; + vec2 subPixelOffset = fract(internalPos) - 0.5; + vec2 sampleDelta = subPixelOffset - jitterPx; + float dist2 = dot(sampleDelta, sampleDelta); + float sampleQuality = exp(-dist2 * 3.0); + float blendFactor = mix(0.03, 0.20, sampleQuality); - // Disocclusion: large clamp distance → rapidly replace stale history - blendFactor = mix(blendFactor, 0.60, clamp(clampDist * 5.0, 0.0, 1.0)); + // Disocclusion: aggressively replace stale history + blendFactor = mix(blendFactor, 0.80, clamp(clampDist * 5.0, 0.0, 1.0)); - // Velocity: higher blend during motion reduces ghosting - float motionMag = length(motion * pc.displaySize.xy); - blendFactor = max(blendFactor, clamp(motionMag * 0.15, 0.0, 0.35)); + // Velocity: strong response during camera/object motion + blendFactor = max(blendFactor, clamp(motionMag * 0.30, 0.0, 0.50)); // Full current frame when history is out of bounds blendFactor = mix(blendFactor, 1.0, 1.0 - historyValid); - vec3 result = mix(historyColor, currentColor, blendFactor); + // Blend in tonemapped space, inverse-tonemap back to linear + vec3 tmResult = mix(tmHistory, tmCurrent, blendFactor); + vec3 result = inverseTonemap(tmResult); + imageStore(historyOutput, outPixel, vec4(result, 1.0)); } diff --git a/assets/shaders/fsr2_accumulate.comp.spv b/assets/shaders/fsr2_accumulate.comp.spv index 2ea3acb515e67eced0532dde96d59d0168498a15..c45903790450995759e02f11421bd1271fcff2ed 100644 GIT binary patch literal 20832 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 literal 18288 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 Xe@@4N`KkV&!m;}6ItktD0-&P`5Wg3qGcZ6 z*w62-fICwgs39t1G+cB)yN#N{DQIcf0*ck EKSfX|vH$=8 delta 300 zcmXYrJqp555QI00F+?OFVxbZ+1}huEM)0Q-YfEbnpq3V5<6n#>YP^MwkaJjj8o`-I z7Cv@o_wDR^x;OW|)U_~K>8TfrNE0w7k`rnVXi6IrrekBF+l8+`sERE?@-h9^8AC<@ z`dFgoBBKpmMvhZ6|FX7wlp9bKOSKQE>D>W4!1)j>HRK~$gGfxJPg##UBQeyBJ-+AZ u3*cF53;7D{Ow%tCNxDv{f~;Z+Uw-WlqGaaO8!;*}hXdW{@Blyju?PMHmnEsetLoadRadius(2); // 5x5=25 tiles — balance between spawn hitches and load time + terrainMgr->setLoadRadius(3); // 7x7=49 tiles — prevents hitches on spawn terrainMgr->setUnloadRadius(7); // Trigger tile streaming for surrounding area