Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/tickets.md
Original file line number Diff line number Diff line change
Expand Up @@ -689,3 +689,54 @@ symbolized stack; fix then.
(profiler overlay/history strings are the heaviest string traffic in the
crashing runs), or an engine-side heap read overrun in a small shared
helper. Audit report: `shooter/docs/audit-round2.md` finding F1.

## EN-021 — SSR + IBL specular exclusive ownership 🟡

**Why.** Round-2 audit (B): compose is `hdr + ssr` and `fs_main_scene`
already adds IBL specular into hdr for metals — pixels with an SSR hit
double-count specular for roughness ≈0.05–0.85, worst for metals at
r≈0.55–0.75 (in the shooter: alien carapaces / weapon per their glTF
factors). Dielectrics are largely starved of IBL spec by design and are
fine; the shooter's custom world materials never call ibl() and are
unaffected.

**Why not a one-liner.** Scaling `ibl_spec` by the complement of the SSR
fade must be PAIRED with an env fallback on SSR miss (today miss = black,
ssgi.rs:1495) or off-screen-reflection pixels lose their specular
entirely — a worse artifact than the double-count. The fallback needs the
env cubemap bound into the SSR pass (bind-group layout change).

**Sketch.** (1) Bind env_tex into the SSR march; on miss return
`sample_env(r, r*max_mip) * fresnel * roughness_fade * strength` instead
of black. (2) In `fs_main_scene`, scale `ibl_spec` by
`1 − (1 − smoothstep(0.5, 0.85, r)) * ssr_strength` (the exact
complement of the SSR shader's own fade). (3) Regenerate goldens; verify
with the audit's metal-ROI protocol (on-hit vs panned-off luma converge
<5%).

**Interim calibration option:** `set_ssr_strength(0.25–0.35)` halves the
overlap engine-wide with zero shader work.

## EN-022 — Motion vectors for material-system draws 🔴

**Why.** Round-2 audit (F8): every material-path draw writes velocity = 0
(terrain/tree/grass/building — the whole static world plus wind sway),
so TAA's motion adaptation (`post.rs` motion_alpha ramp) never engages
for the world: camera translation is covered only by depth reprojection
and object sway not at all. This is the primary mechanism behind TSR 0.5
shimmer on thin grass + sway ghosting, and it blocks any future
motion-blur quality on the world.

**Scope.** Per-draw previous-frame model matrix (the per-draw UBO slot
already carries the current one; the shadow path already keeps a CPU
copy in `MaterialDrawCommand.model`), previous view-proj in PerView (the
core path has it), and a velocity write in the material ABI's OpaqueOut
path — plus the wind displacement evaluated at previous-frame time in
foliage vertex shaders so sway produces real motion vectors. Roughly:
ABI struct + material_system plumbing + 4 world materials + goldens.

**Acceptance.** Strafe at the audit's S7 pose: thin-grass shimmer index
(frame-diff metric) drops materially; wind sway no longer ghosts;
`post.rs` motion clamp engages on world pixels (debug: motion_alpha
visualization).

8 changes: 8 additions & 0 deletions native/shared/src/ffi_core/visual.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,14 @@ macro_rules! __bloom_ffi_visual {
})
}

// bloom_set_sharpen_strength [round-2 audit F8]
#[no_mangle]
pub extern "C" fn bloom_set_sharpen_strength(strength: f64) {
$crate::ffi::guard("bloom_set_sharpen_strength", move || {
engine().renderer.set_sharpen_strength(strength as f32);
})
}

// bloom_set_sun_shafts [source: macos]
#[no_mangle]
pub extern "C" fn bloom_set_sun_shafts(strength: f64, decay: f64, r: f64, g: f64, b: f64) {
Expand Down
6 changes: 3 additions & 3 deletions native/shared/src/renderer/draw2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ impl Renderer {

pub fn draw_rect(&mut self, x: f64, y: f64, w: f64, h: f64, r: f64, g: f64, b: f64, a: f64) {
self.ensure_draw_state(0);
let color = Self::color_to_f32(r, g, b, a);
let color = Self::color_to_f32_srgb(r, g, b, a);
let base = self.vertices_2d.len() as u32;
let (x, y, w, h) = (x as f32, y as f32, w as f32, h as f32);

Expand All @@ -32,7 +32,7 @@ impl Renderer {

pub fn draw_line(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, thickness: f64, r: f64, g: f64, b: f64, a: f64) {
self.ensure_draw_state(0);
let color = Self::color_to_f32(r, g, b, a);
let color = Self::color_to_f32_srgb(r, g, b, a);
let dx = (x2 - x1) as f32;
let dy = (y2 - y1) as f32;
let len = (dx * dx + dy * dy).sqrt();
Expand All @@ -53,7 +53,7 @@ impl Renderer {

pub fn draw_circle(&mut self, cx: f64, cy: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) {
self.ensure_draw_state(0);
let color = Self::color_to_f32(r, g, b, a);
let color = Self::color_to_f32_srgb(r, g, b, a);
let segments = 36u32;
let base = self.vertices_2d.len() as u32;
let (cx, cy, radius) = (cx as f32, cy as f32, radius as f32);
Expand Down
38 changes: 33 additions & 5 deletions native/shared/src/renderer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,16 @@ struct CachedModelDraw {
// Renderer
// ============================================================

/// 0-255 sRGB channel → linear f32. The 2D pass renders into an sRGB
/// swapchain view whose hardware encode expects LINEAR shader output;
/// passing the sRGB byte value straight through double-encoded every 2D
/// color (washed-bright HUD, gamma-skewed AA edges — round-2 audit F5).
/// Alpha stays linear by definition and is NOT decoded.
pub(crate) fn srgb_u8_to_linear(c: f64) -> f32 {
let c = (c / 255.0).clamp(0.0, 1.0);
(if c <= 0.04045 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) }) as f32
}

pub struct Renderer {
pub device: wgpu::Device,
pub queue: wgpu::Queue,
Expand Down Expand Up @@ -7851,6 +7861,14 @@ impl Renderer {
self.grain_strength = strength.max(0.0);
}

/// Composite-pass unsharp mask. Default 0.8; 0 disables the 4 extra
/// HDR taps + extra tonemap entirely. Round-2 audit: this was
/// hardcoded with no runtime control while visibly haloing
/// silhouettes at 4K output (F3/F8).
pub fn set_sharpen_strength(&mut self, strength: f32) {
self.sharpen_strength = strength.max(0.0);
}

/// Sun shaft (screen-space god ray) strength. 0 (default) = off.
/// 0.4 = subtle haze, 1.0+ = obvious cinematic shafts. The
/// shafts are sampled from the depth buffer along a screen-space
Expand Down Expand Up @@ -9841,14 +9859,24 @@ impl Renderer {
}
}

/// Raw 0-255 → 0-1, no gamma. Used by the 3D immediate batch and
/// model tints, whose values feed the linear HDR pipeline directly —
/// changing their interpretation would silently re-tint every
/// existing drawCube/drawModel call in shipped games.
fn color_to_f32(r: f64, g: f64, b: f64, a: f64) -> [f32; 4] {
[(r / 255.0) as f32, (g / 255.0) as f32, (b / 255.0) as f32, (a / 255.0) as f32]
}

/// 2D variant: sRGB-decodes rgb so the sRGB swapchain view's hardware
/// encode does not double-encode (see `srgb_u8_to_linear`).
fn color_to_f32_srgb(r: f64, g: f64, b: f64, a: f64) -> [f32; 4] {
[srgb_u8_to_linear(r), srgb_u8_to_linear(g), srgb_u8_to_linear(b), (a / 255.0) as f32]
}


pub fn draw_triangle(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, r: f64, g: f64, b: f64, a: f64) {
self.ensure_draw_state(0);
let color = Self::color_to_f32(r, g, b, a);
let color = Self::color_to_f32_srgb(r, g, b, a);
let base = self.vertices_2d.len() as u32;

self.vertices_2d.push(Vertex2D { position: [x1 as f32, y1 as f32], uv: [0.0, 0.0], color });
Expand All @@ -9860,7 +9888,7 @@ impl Renderer {

pub fn draw_poly(&mut self, cx: f64, cy: f64, sides: f64, radius: f64, rotation: f64, r: f64, g: f64, b: f64, a: f64) {
self.ensure_draw_state(0);
let color = Self::color_to_f32(r, g, b, a);
let color = Self::color_to_f32_srgb(r, g, b, a);
let n = sides as u32;
if n < 3 { return; }
let base = self.vertices_2d.len() as u32;
Expand Down Expand Up @@ -9905,7 +9933,7 @@ impl Renderer {
pub fn draw_texture(&mut self, bind_group_idx: u32, x: f64, y: f64, tint_r: f64, tint_g: f64, tint_b: f64, tint_a: f64) {
let (tw, th) = self.texture_sizes.get(bind_group_idx as usize).copied().unwrap_or((0, 0));
if tw == 0 { return; }
let color = Self::color_to_f32(tint_r, tint_g, tint_b, tint_a);
let color = Self::color_to_f32_srgb(tint_r, tint_g, tint_b, tint_a);
self.draw_textured_quad(x as f32, y as f32, tw as f32, th as f32, 0.0, 0.0, 1.0, 1.0, color, bind_group_idx);
}

Expand All @@ -9917,7 +9945,7 @@ impl Renderer {
) {
let (tw, th) = self.texture_sizes.get(bind_group_idx as usize).copied().unwrap_or((0, 0));
if tw == 0 { return; }
let color = Self::color_to_f32(tint_r, tint_g, tint_b, tint_a);
let color = Self::color_to_f32_srgb(tint_r, tint_g, tint_b, tint_a);
let u0 = src_x as f32 / tw as f32;
let v0 = src_y as f32 / th as f32;
let u1 = (src_x + src_w) as f32 / tw as f32;
Expand All @@ -9934,7 +9962,7 @@ impl Renderer {
) {
let (tw, th) = self.texture_sizes.get(bind_group_idx as usize).copied().unwrap_or((0, 0));
if tw == 0 { return; }
let color = Self::color_to_f32(tint_r, tint_g, tint_b, tint_a);
let color = Self::color_to_f32_srgb(tint_r, tint_g, tint_b, tint_a);
let u0 = src_x as f32 / tw as f32;
let v0 = src_y as f32 / th as f32;
let u1 = (src_x + src_w) as f32 / tw as f32;
Expand Down
15 changes: 5 additions & 10 deletions native/shared/src/renderer/shaders/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -849,18 +849,13 @@ fn fs_main_scene(in: VertexOutputScene) -> SceneOut {
// (the bright rim glow when the sun is behind a tree). Gated on the
// alpha-cutoff so only cut-out foliage materials get it; opaque surfaces
// (cutoff == 0) are unaffected. Matches shade_foliage's transmission term.
// Round-2 audit: this block was pasted TWICE (1.7x strength) and ran
// unshadowed — a canopy in another tree's shadow still glowed at full
// transmission. De-duplicated and multiplied by the sun shadow factor.
if (alpha_cutoff > 0.0) {
let trans = pow(max(dot(v, -legacy_dir), 0.0), 3.0) * 0.85;
lit += base_color * lighting.light_color.rgb * lighting.light_dir.w * trans;
}

// Foliage backlit transmission — sun bleeding THROUGH alpha-cut leaf cards
// (the bright rim glow when the sun is behind a tree). Gated on the
// alpha-cutoff so only cut-out foliage materials get it; opaque surfaces
// (cutoff == 0) are unaffected. Matches shade_foliage's transmission term.
if (alpha_cutoff > 0.0) {
let trans = pow(max(dot(v, -legacy_dir), 0.0), 3.0) * 0.85;
lit += base_color * lighting.light_color.rgb * lighting.light_dir.w * trans;
lit += base_color * lighting.light_color.rgb * lighting.light_dir.w * trans
* direct_shadow;
}

let dir_count = u32(lighting.dir_light_count.x);
Expand Down
18 changes: 12 additions & 6 deletions native/shared/src/text_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,13 @@ impl TextRenderer {
None => return,
};

// sRGB-decode to linear like every other 2D vertex color — the
// swapchain view's hardware encode expects linear shader output
// (see renderer::srgb_u8_to_linear).
let color = [
(r / 255.0) as f32,
(g / 255.0) as f32,
(b / 255.0) as f32,
crate::renderer::srgb_u8_to_linear(r),
crate::renderer::srgb_u8_to_linear(g),
crate::renderer::srgb_u8_to_linear(b),
(a / 255.0) as f32,
];

Expand Down Expand Up @@ -419,10 +422,13 @@ impl TextRenderer {
None => return,
};

// sRGB-decode to linear like every other 2D vertex color — the
// swapchain view's hardware encode expects linear shader output
// (see renderer::srgb_u8_to_linear).
let color = [
(r / 255.0) as f32,
(g / 255.0) as f32,
(b / 255.0) as f32,
crate::renderer::srgb_u8_to_linear(r),
crate::renderer::srgb_u8_to_linear(g),
crate::renderer::srgb_u8_to_linear(b),
(a / 255.0) as f32,
];

Expand Down
Binary file modified native/shared/tests/golden/shapes_2d.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,13 @@
],
"returns": "void"
},
{
"name": "bloom_set_sharpen_strength",
"params": [
"f64"
],
"returns": "void"
},
{
"name": "bloom_set_sun_shafts",
"params": [
Expand Down
10 changes: 10 additions & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ declare function bloom_set_fog(r: number, g: number, b: number, density: number,
declare function bloom_set_chromatic_aberration(strength: number): void;
declare function bloom_set_vignette(strength: number, softness: number): void;
declare function bloom_set_film_grain(strength: number): void;
declare function bloom_set_sharpen_strength(strength: number): void;
declare function bloom_set_sun_shafts(strength: number, decay: number, r: number, g: number, b: number): void;
declare function bloom_set_auto_exposure(on: number): void;
declare function bloom_set_taa_enabled(on: number): void;
Expand Down Expand Up @@ -265,6 +266,15 @@ export function setFilmGrain(strength: number): void {
bloom_set_film_grain(strength);
}

/**
* Composite unsharp-mask strength. Engine default 0.8; 0 disables the
* sharpen taps entirely. At high output resolutions the default visibly
* halos high-contrast silhouettes — tune per game.
*/
export function setSharpenStrength(strength: number): void {
bloom_set_sharpen_strength(strength);
}

/** Screen-space sun shafts (god rays). strength 0 = off. */
export function setSunShafts(strength: number, decay: number, r: number, g: number, b: number): void {
bloom_set_sun_shafts(strength, decay, r, g, b);
Expand Down