From 90ec3e6adf1f5e7f576bac87ca7515793edd3ab3 Mon Sep 17 00:00:00 2001 From: ReaperOfVeriod Date: Wed, 14 Oct 2026 04:34:52 +0200 Subject: [PATCH] The texture sampler was Repeat while its comment claimed it clamped Wiring the first gs rule into the renderer, and it turned up a bug that had been sitting in plain sight: // Only mip level 0 is decoded, so minification must clamp rather than // invent a level the disc does not have here. ImageSamplerDescriptor { mag_filter: ImageFilterMode::Linear, ..default() // <-- address_mode is Repeat } The comment describes clamping. The code was doing the opposite: `..default()` supplies bevy's default address mode, which is ImageAddressMode::Repeat. Nothing here ever set an address mode, so every texture was sampling with Repeat while a comment three lines up claimed it was not. That is the GS rule I wrote in 81ec178 saying out loud -- CLAMP, with 1.0 held at the far edge -- and the renderer was doing the other thing. The seam shows up as a patch running off its texture wrapping onto the opposite edge instead of repeating the border texel, which is exactly the kind of thing that reads as "the texture decoded wrong" and sends you looking in the format parser instead of at one line of sampler setup. Also set mipmap_filter to Nearest. Only level 0 is decoded, so there is no chain to interpolate across and the filter cannot matter; Nearest is the honest setting until levels exist, and the comment now says that gs::mip_level returns 0 for every patch until they are decoded, rather than implying a selection is happening. Worth noting how this was found. I went looking for where clamp01 should live and found the answer was "somewhere that already claims to do it, and doesn't". A comment asserting behaviour is not evidence of behaviour -- the comment here was specific and plausible and wrong, which is worse than no comment, because it stops anyone checking. Two API corrections along the way, both mine: ImageSamplerDescriptor has no `address_mode` field -- it is address_mode_u and address_mode_v separately -- and the engine is on bevy 0.19 rather than raw wgpu, so the GS rules reach the GPU through bevy's sampler. The roadmap now says so instead of implying a wgpu integration that does not exist. 278 tests, clippy clean. --- crates/gc-engine/src/main.rs | 17 +++++++++++++---- docs/roadmap.md | 8 +++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/crates/gc-engine/src/main.rs b/crates/gc-engine/src/main.rs index 72b0480..77aa541 100644 --- a/crates/gc-engine/src/main.rs +++ b/crates/gc-engine/src/main.rs @@ -19,7 +19,7 @@ use std::path::PathBuf; use anyhow::{Context, Result}; use bevy::asset::RenderAssetUsages; -use bevy::image::{ImageFilterMode, ImageSampler, ImageSamplerDescriptor}; +use bevy::image::{ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor}; use bevy::prelude::*; use bevy::render::mesh::{Indices, PrimitiveTopology}; use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat}; @@ -386,11 +386,20 @@ fn image_of(t: &gc_formats::texture::Texture) -> Image { TextureFormat::Rgba8UnormSrgb, RenderAssetUsages::default(), ); - // Only mip level 0 is decoded, so minification must clamp rather than - // invent a level the disc does not have here. + // The GS clamps; bevy's default is Repeat. This was `..default()` with a + // comment claiming the sampler clamped, which it did not -- the default is + // ImageAddressMode::Repeat. A tile edge therefore wrapped onto its opposite + // edge instead of repeating the border texel, which is the seam you see when + // a patch runs off its texture. See gc_formats::gs::clamp01. + // + // Only mip level 0 is decoded, so there is no chain to interpolate: the + // mipmap filter is Nearest because it samples that single level either way, + // and gs::mip_level would return 0 for every patch until levels are decoded. image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor { label: Some("tfrag texture".into()), - mipmap_filter: ImageFilterMode::Linear, + address_mode_u: ImageAddressMode::ClampToEdge, + address_mode_v: ImageAddressMode::ClampToEdge, + mipmap_filter: ImageFilterMode::Nearest, mag_filter: ImageFilterMode::Linear, ..default() }); diff --git a/docs/roadmap.md b/docs/roadmap.md index ad7d32f..3f2f2e3 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -717,9 +717,11 @@ lookup, and it is the main thing inherited from ReRAC. the GS's `AFAIL` bit reads as "fail if above the reference" and on the hardware means the opposite -- it discards where alpha is **below**; and the opaque bit must be kept out of the comparison, or `0x80` satisfies every - threshold and nothing is ever discarded. **No wgpu work has started** -- - this is the rule layer the renderer will feed it, done first because it is - checkable without a GPU. + threshold and nothing is ever discarded. **The engine is on bevy 0.19, not raw wgpu**, so these rules reach the GPU + through bevy's sampler rather than wgpu directly. One is now wired: the + texture sampler was `..default()` with a comment claiming it clamped, and + bevy's default is `Repeat` -- see `image_of` in `crates/gc-engine`. + **No draw-path or shader work has started.** - [ ] VU0/VU1 skinning and lighting on the PS2 float model - [ ] LOD/morph/cull per `TfragProc` and `TieProc` - [ ] Occlusion culling, fog, shadows