Skip to content

Emulate N64 memory coverage (memcvg) - #2983

Open
gonetz wants to merge 1 commit into
masterfrom
emulate_memory_coverage
Open

Emulate N64 memory coverage (memcvg)#2983
gonetz wants to merge 1 commit into
masterfrom
emulate_memory_coverage

Conversation

@gonetz

@gonetz gonetz commented Aug 9, 2026

Copy link
Copy Markdown
Owner

The RDP keeps a 3 bit coverage value per pixel in the extra bits of 9 bit RDRAM. The blender reads it back as memory coverage and uses it to decide whether to blend at all, as the G_BL_A_MEM blend factor and as the source of the coverage written back to the framebuffer. GLideN64 calculated pixel coverage from barycentric coordinates but threw it away, so none of that could be emulated.

Store the coverage in a screen sized R8UI image and read/write it in the fragment shader under fragment shader interlock, so that overlapping primitives see each other's coverage in draw order. The feature needs GL_ARB_fragment_shader_interlock, GL_NV_fragment_shader_interlock or GL_INTEL_fragment_shader_ordering plus image load/store; it is disabled gracefully when the driver has none of them. An image is used rather than a second color attachment because dual source blending, which the blender relies on, forbids multiple render targets.

Emulated on top of that, following the hardware:

  • overflow/prewrap = (memcvg + cvg) & 8 and blend_en = force_blend || (!overflow && antialias_en && farther)
  • the CLR_ON_CVG branch of the blender, which selects the M input instead of the blend equation when coverage does not overflow
  • partial reject (dontblend), which passes the P input through for opaque pixels of a src alpha / inverse src alpha blender
  • the G_BL_A_MEM blend factor, fed with memcvg << 5
  • all four cvg_dest modes: CLAMP, WRAP, ZAP and SAVE
  • fill rectangles, which write coverage 7 or 0 depending on the low bit of the fill color
  • rejected pixels leave the stored coverage untouched

Coverage can be enebled along with N64 depth compare. Both need the same critical section, so they now share one interlock block placed at the top level of main() and ordered the way the hardware orders them: depth compare first, its "farther" result feeding blend_en, then the blender, then the coverage write back, then a single deferred discard.

One deliberate deviation: on a full coverage pixel with blending off the hardware writes cvg-1, because the neighbouring primitive is guaranteed to write the same pixel again and to top the coverage up. GPU rasterization gives a pixel to one triangle only, so that second write never happens and the lowered value would leak into every later read of memory coverage along internal edges of a mesh, producing cracks. max(cvg-1, memcvg) reproduces the state the hardware reaches after both primitives are drawn. The cost is that a silhouette edge over an opaque surface keeps full coverage; nothing reads that value yet. See the comment in glsl_CombinerProgramBuilderCommon.cpp.

Coverage scaling for ZMODE_INTERPENETRATING is not emulated.

Also adds the debug.displayCoverage debug option, which shows the content of the coverage buffer on screen the way G_RM_VISCVG does on hardware. It works only when generalEmulation.enableCoverage is set.

The RDP keeps a 3 bit coverage value per pixel in the extra bits of 9 bit
RDRAM. The blender reads it back as memory coverage and uses it to decide
whether to blend at all, as the G_BL_A_MEM blend factor and as the source of
the coverage written back to the framebuffer. GLideN64 calculated pixel
coverage from barycentric coordinates but threw it away, so none of that
could be emulated.

Store the coverage in a screen sized R8UI image and read/write it in the
fragment shader under fragment shader interlock, so that overlapping
primitives see each other's coverage in draw order. The feature needs
GL_ARB_fragment_shader_interlock, GL_NV_fragment_shader_interlock or
GL_INTEL_fragment_shader_ordering plus image load/store; it is disabled
gracefully when the driver has none of them. An image is used rather than a
second color attachment because dual source blending, which the blender
relies on, forbids multiple render targets.

Emulated on top of that, following the hardware:
- overflow/prewrap = (memcvg + cvg) & 8 and
  blend_en = force_blend || (!overflow && antialias_en && farther)
- the CLR_ON_CVG branch of the blender, which selects the M input instead of
  the blend equation when coverage does not overflow
- partial reject (dontblend), which passes the P input through for opaque
  pixels of a src alpha / inverse src alpha blender
- the G_BL_A_MEM blend factor, fed with memcvg << 5
- all four cvg_dest modes: CLAMP, WRAP, ZAP and SAVE
- fill rectangles, which write coverage 7 or 0 depending on the low bit of
  the fill color
- rejected pixels leave the stored coverage untouched

Coverage can be enebled along with N64 depth compare. Both need
the same critical section, so they now share one interlock block placed at
the top level of main() and ordered the way the hardware orders them: depth
compare first, its "farther" result feeding blend_en, then the blender, then
the coverage write back, then a single deferred discard.

One deliberate deviation: on a full coverage pixel with blending off the
hardware writes cvg-1, because the neighbouring primitive is guaranteed to
write the same pixel again and to top the coverage up. GPU rasterization
gives a pixel to one triangle only, so that second write never happens and
the lowered value would leak into every later read of memory coverage along
internal edges of a mesh, producing cracks. max(cvg-1, memcvg) reproduces
the state the hardware reaches after both primitives are drawn. The cost is
that a silhouette edge over an opaque surface keeps full coverage; nothing
reads that value yet. See the comment in glsl_CombinerProgramBuilderCommon.cpp.

Coverage scaling for ZMODE_INTERPENETRATING is not emulated.

Also adds the debug.displayCoverage debug option, which shows the
content of the coverage buffer on screen the way G_RM_VISCVG does on
hardware. It works only when generalEmulation.enableCoverage is set.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant