Repository navigation
GX fog on the NV2A (per-vertex, from GX's own fog registers) - #1
Merged
Merged
Conversation
GXSetFog was recorded and ignored (fog off). Now every GX fog type is drawn: - nv2a_fog.c rebuilds the registers GXSetFog writes (A and C cut to 11 mantissa bits, b_mag, b_shift; libogc GX_SetFog) and turns them into two vertex-program rows: GX's clamp(ze - C) is a ratio of two linear functions of the view-space position, since the EFB depth is (depth row . P) / (w row . P). The vertex program (VpKey.fog: linear, exp, exp2) evaluates it per vertex and, for the EXP types, the curve with expp; rows VPC_FOG..+2. - The NV2A's fog unit runs LINEAR with FOG_PARAMS (1, 1, 0), which passes oFog.x through (the linear mode subtracts 1), clamped per pixel; the final combiner's CW0 becomes F * fog colour + (1 - F) * PREV, alpha untouched. - Fog state is sent only when GXSetFog changed something (it now returns early on equal values like the other setters) or the projection changed while fog is on. Unfogged vertex programs and CW0 are unchanged. - tools/xbox/test_fog.py checks the port against a reference GX (libogc's registers, Dolphin's per-pixel formula on the 24-bit depth) for every type, both projections and several cameras including Melee's 0.1..16384: within 1/255 of GX over one of its depth steps at every vertex; LIN edges where GX's b_mag rounding doesn't bend ze; the final combiner's blend. - test_vp_encoder.py covers expp and a MAD reading one constant twice. GXSetFogRangeAdj stays unapplied. Docs: renderer.md (Fog section, rows), decisions.md, testing.md, roadmap.md; CI runs the new test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016pxfm8RyF7fCMgQdaKigij
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Until now
GXSetFogrecorded its arguments and the back end ignored them. This PR draws every GX fog type (GX_FOG_PERSP_*/GX_FOG_ORTHO_*× LIN, EXP, EXP2, REVEXP, REVEXP2;GX_FOG_NONEis off).GX semantics (libogc
GX_SetFog, DolphinPixelShaderGen::WriteFog)GXSetFogwrites A and C as floats cut to 11 mantissa bits. Perspective fog also gets B as a 24-bitb_mag(B_mant * 8388638) plus ab_shift.ze = A·2²⁴ / (b_mag − (Zs >> b_shift)), which is eye depth / (end − start) when fog near/far match the projection's;ze = A·Zs/2²⁴.x = clamp(ze − C, 0, 1)and the curve: LINx, EXP1−2^(−8x), EXP21−2^(−8x²), REVEXP2^(−8(1−x)), REVEXP22^(−8(1−x)²).(rgb·(256−F₂₅₆) + fog·F₂₅₆) >> 8.Mapping onto the NV2A
nv2a_fog.c(new). Rebuilds exactly the registersGXSetFogwould write. Zs/2²⁴ = (depth row · P)/(w row · P), soclampinputze − C(or1 − (ze − C)for REV) is a ratio of two linear functions of the view-space position. Those two rows plus a curve row go into constantsVPC_FOG..+2(134–136).VpKey.fog(off / linear / exp / exp2, taking a former pad byte) adds 4 (LIN) to 10 (EXP2) instructions writingoFog.x. EXP types clamp and useexpp.FOG_MODELINEAR withFOG_PARAMS (1, 1, 0)passesoFog.xthrough. The linear mode computesp0 + p1·c − 1; this is the setup nxdk_pgraph_tests uses for vertex-shader fog, and it's what xemu implements. The gen mode doesn't matter with a vertex program. The NV2A's EXP modes aren't used: they're evaluated per vertex as well, and hardware measures EXP's zero point at ~1.51.SET_FOG_COLORis sent ABGR with alpha 0.XGX_DIRTY_FOG, or on a projection change while fog is on.GXSetFognow returns early on unchanged values like the other setters, because HSD calls it per camera pass and per particle kind and each call used to flush the waiting batch. No per-draw CPU cost is added.xgx_draw, the vertex-buffer paths,gx_vtx.candgx_tex.care untouched: the fog work hangs offemit_fixed,build_proj,derive_vkandemit_combiners.rp->cw0, byte for byte. Fog-off vertex programs are identical to before: I checked 200,000 random keys, old generator against new.Known limits
zeis affine in eye depth. With Melee's 0.1..16384 game camera, GX's ownb_magrounding (8388638/2²³) grows its denominator by ~57% from near to far. A polygon spanning a long depth range then gets less fog mid-span than on the GameCube: up to ~23/255 on an edge from depth 2000 to 14000 through a narrow fog range. EXP curves are linear between vertices.zeup to 1.57× smaller at the far plane than the textbook formula, and the port matches that at every vertex.GXSetFogRangeAdjisn't applied. It's a horizontal radial-distance correction that HSD sets when a fog has a FogAdj descriptor.Tests
tools/xbox/test_fog.py+tests/xbox/test_fog.c.build_proj's rows →fog_setup→ the vertex program's float arithmetic → the NV2A LINEAR factor.ze(<1%). The Melee-camera edge deviation is reported, not asserted.test_vp_encoder.pynow also coversexppand a MAD reading one constant row twice: 17 instructions match nv2a-vsh.test_tex_convert.pypasses.tools/lower/test_lower.pywas not run. It needs LLVM 21 +disc_lower; the host has LLVM 18,disc_lowerdoesn't build against 18's API, and apt.llvm.org is blocked by this environment's egress policy. This PR doesn't touchtools/loweror any lowered game code.Build
tools/xbox/docker/build.shwas not run, so the XBE link is unverified here. The SDK image build failed: Docker Hub rate-limiteddebian:trixie, and apt inside the container was blocked by the proxy. This PR's CI buildsdefault.xbeand runs the host tests, so the link check happens there.As a substitute I compiled against nxdk at the pinned SHA using host clang 18 with nxdk-cc's flags and the project's hw flags:
nv2a.c(with and without-DXGX_DEBUG_TRACE),nv2a_fog.c,nv2a_vp.candnv2a_rc.ccompile warning-free under-Wallfori386-pc-win32.nv2a_fog.objneeds only__fltused.gx_state.ccompiles with the SDK's game-triple flags.No imported game code was edited (no
PORT:changes), andxgx.his unchanged.Please check on the console against Dolphin
-DXGX_DEBUG_TRACEnow logs each draw's fog state.Ground_801C1E94loads each stage's fog descriptor (start/end scaled by the stage'sparam->y; its colour is also the clear colour), andGround_801C1E2Capplies it per camera pass. Compare distant backgrounds, large floor or background polygons (the per-vertex limit above) and particles on several stages. Final Destination turns its fog off during its transition (grlast.c). Particles whose kind hasDispFog(psdisp.c) switch fog per kind.ScTitle_fog).gm_1832.c) and the All-Star intro (gm_186E.c).vi0102,vi0401,vi0501,vi0502,vi0801, andvi1201v1/v2(Bowser).gmtou_*.c), the 1P ending display (gmregenddisp.c), the staff roll.ScMenDisplay_fog,tydisplay.c).toy.c's own white fog is debug-ROM only.[PERF]shouldn't move.Docs:
renderer.md(new Fog section, constant rows),decisions.md,testing.md,roadmap.md,CLAUDE.mdtest list.🤖 Generated with Claude Code
https://claude.ai/code/session_016pxfm8RyF7fCMgQdaKigij
Generated by Claude Code