Skip to content

feat(shader): emulate fragment barycentrics with a generated geometry stage - #1547

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
russhil:feat/shader-barycentric-emulation
Oct 10, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
russhil:feat/shader-barycentric-emulation

Conversation

@russhil

@russhil russhil commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

What

Without VK_KHR_fragment_shader_barycentric (lavapipe in CI, NVIDIA before Turing, older Intel), the recompiler dropped v_interp_p1_f32 and had v_interp_p2_f32 read the attribute Vulkan interpolated, but it never wrote the I/J VGPRs. So on main:

  • a pixel shader that reads I/J for anything other than v_interp_p1/p2 of the same pair (EvaluateAttributeAt*-style offsets, custom weights, exporting them) silently gets 0;
  • v_interp_mov_f32 of P10/P20, or of P0 of an interpolated input, needs PerVertexKHR and fails pipeline validation ("device does not support enabled VK_KHR_fragment_shader_barycentric");
  • inputs that share a slot with different interpolation fail the same way.

This removes the silent stub listed in TechnicalDebt and replaces it with exact emulation:

  1. Translation. The fallback now loads I/J like the barycentric path. v_interp_p1_f32 writes an InterpolateHostP1 witness, and v_interp_p2_f32 adds a side-effecting InterpolateHostP2(attr, chan, J, partial) witness before its usual host-interpolated write.
  2. HostInterpolationChecker, which runs after the SSA/fold/DCE/read-lane/masked-select passes:
    • It checks that every P2 reads its J, and that its p1's I comes from a barycentric of the parameter's kind.
    • It checks that every p1 result only reaches the matching p2, either directly or through a write masked by the p2's EXEC.
    • It checks that no parameter is read as raw vertices and no slot mixes interpolations.
    • It then lowers the witnesses: the masked p1 write keeps the old register value in inactive lanes, as on the barycentric path.
    • It reruns DCE and masked-select elimination, and accepts the program only if no barycentric read remains.
    • Accepted programs give the same SPIR-V as before. Overwriting or accumulating into the I/J VGPRs after the interpolation stays on this path.
  3. Emulated programs. Anything else is translated with the barycentric semantics and marked as emulated:
    • BaryCoordKHR/BaryCoordNoPerspKHR become vec3 inputs; the linear one is NoPerspective.
    • PerVertexKHR parameters become Flat vec4[3] inputs.
    • These inputs take the free locations chosen by LayoutBarycentricEmulation.
    • RecompileResult::barycentricEmulation records this; the shader disk cache format goes to 8.
  4. BuildBarycentricGeometryShader generates a pass-through geometry stage: triangles in, a three-vertex strip out. It copies the position and the plain parameters, writes the one-hot weights (1,0,0)/(0,1,0)/(0,0,1), and copies the three vertices' values into each per-vertex array. The rasterizer then interpolates exactly the weights BaryCoordKHR gives, with the same vertex order (gl_in[0] stays the provoking vertex). The driver inserts the stage between the vertex and fragment shaders, like the rect-list stages. ValidateShaders accepts [Vertex, Geometry, Fragment] only for an emulated fragment shader and triangle topologies. The pipeline and validation keys treat the stage as generated, and Pipeline requires geometryShader.

Limits. Tessellation, mesh and rect-list pipelines with such a shader throw, as do other primitive types, more than 32 fragment input locations, and interfaces past the device's geometry limits. These limits are recorded in TechnicalDebt.

Behaviour that does not change:

  • Devices with the extension are unaffected: the checker and the SPIR-V changes run only without it.
  • Shaders that only use v_interp_p1/p2 keep host interpolation and their previous SPIR-V.

Sources:

  • v_interp_p1_f32/v_interp_p2_f32/v_interp_mov_f32 (P0, P10, P20, I/J): AMD "RDNA 2" Instruction Set Architecture, section "Parameter Interpolation".
  • Barycentric weights and per-vertex order: SPV_KHR_fragment_shader_barycentric, VK_KHR_fragment_shader_barycentric.
  • Triangle vertex order into a geometry shader and the provoking vertex: Vulkan specification, "Primitive Topologies" and "Geometry Shading".
  • Interpolation decided by the fragment input decoration: Vulkan specification, "Interpolation Decorations".

Tested

Ubuntu (Linux 7.0, GCC 15.2.0), Release, -DBUILD_TESTING=ON, lavapipe (Mesa, LLVM 21), which has no VK_KHR_fragment_shader_barycentric but has geometryShader. No title or hardware GPU.

  • New agc_driver_pixel_barycentric_emulation (GPU, lavapipe). It draws a triangle whose vertices have w = 1, 2.5 and 0.5 into a 64x48 R32G32B32A32 float target:
    • A pixel shader that exports v0, v1 with PERSP_CENTER, then with LINEAR_CENTER, matches the perspective-correct and the screen-space barycentrics computed on the CPU within 2.5e-4 at all 932 interior pixels (hardware snaps window vertices to 1/256 pixel before computing the weights, lavapipe does not).
    • v_interp_mov_f32 P10, P20 and P0 of an interpolated input give exactly x1-x0, x2-x0 and x0.
    • Plain v_interp_p1/p2 keeps host interpolation and matches the perspective-correct value.
    • On main the same exports read 0 at every pixel (e.g. pixel (32,24): 0 0 0 0, expected I=0.137, J=0.717), and the v_interp_mov draw throws.
  • agc_driver_graphics (barycentricEmulationTests), CPU only:
    • the location layout and its 32-location limit;
    • the emulated inputs (no FragmentBarycentricKHR, no BaryCoordKHR, flat arrays);
    • shaders that overwrite or accumulate into the I/J VGPRs after interpolating stay unemulated;
    • a changed I, a p2 under a wider EXEC than its p1 and shared slots are emulated (the EXEC case fails without its check);
    • ValidateShaders accepts the three-stage pipeline for triangle lists and strips and refuses points or a non-emulated fragment shader;
    • the geometry stage builder refuses missing geometry support, device limits and parameters the vertex shader does not export.
  • agc_shader_disk_cache: the new result fields round-trip.
  • Full ctest on this branch (rebased on main 6b56175): 480/480 passed, 24 GPU tests skipped by lavapipe (subgroup size 8), the same set as on main.
  • python3 tools/check_conventions.py --base upstream/main: passed.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes

@russhil

russhil commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

On the overlap with #1252: both PRs add one call after rectListTests(); in main() of tests/Graphics.cpp (#1252 floatControlsModeTests();, this one barycentricEmulationTests();). The changes are otherwise independent, so whichever merges second keeps both calls; I will rebase this one if #1252 lands first.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 8, 2026
@russhil
russhil force-pushed the feat/shader-barycentric-emulation branch from 9032b65 to 54e5719 Compare October 8, 2026 14:33
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 8, 2026
@yahya308

yahya308 commented Oct 8, 2026

Copy link
Copy Markdown

Tested on top of main 9e2ffe0d, Windows 11, MinGW-w64 GCC 16.2.0, full ctest with ANYPS5_REQUIRE_VULKAN=1 --repeat until-fail:3, NVIDIA RTX 4070 Laptop (32.0.16.1074) and AMD Radeon 780M (32.0.31041.1004). vopc_compare_table fails on the 780M on main too (#825, #1473).

Head 54e5719b. All tests outside this PR match main, but the new agc_driver_pixel_barycentric_emulation fails on both GPUs with the same value:

PERSP_CENTER I/J: pixel (53, 36) channel 1 is 0.237278, expected 0.237381

0.237278 is what Barycentrics() returns when the window-space corners are snapped to 1/256 pixel (8 subpixel bits) before the edge functions. The unsnapped reference differs from the snapped one by up to 1.43e-4 in the perspective weights (19 of the 932 interior pixels exceed 1e-4) and by 4.2e-5 in the linear ones. Locally, either change makes the test pass on both GPUs (932 perspective and 932 linear pixels, plus the v_interp_mov and p1/p2 checks):

  • snapping window[i] to 1/256 in Barycentrics(), or
  • keeping the reference and raising the two 1e-4f tolerances to 2.5e-4f.

I did not check lavapipe. If it weights unsnapped positions, the snapped reference could fail there, so the tolerance change may be the safer one.

AI-assisted: yes (Claude Code).

@russhil
russhil force-pushed the feat/shader-barycentric-emulation branch from 54e5719 to 1e08abb Compare October 8, 2026 15:40
@russhil

russhil commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @yahya308, confirmed: with window vertices snapped to 1/256 my reference gives the same 0.237278 at (53, 36), and up to 1.43e-4 off the unsnapped weights. I took the tolerance option (both checks now 2.5e-4, 1e08abb, rebased on main) because snapping the reference fails on lavapipe, which reads 0.237381 there: llvmpipe snaps to 1/256 only for coverage (lp_rast.h FIXED_ORDER 8, subpixel_snap in lp_setup_tri.c) and builds the interpolation planes from the float positions (init_args in lp_state_setup.c). Full suite 464/464 on lavapipe.

@github-actions github-actions Bot added build shader core/shader docs agc libSceAgc* needs rebase Conflicts with main, rebase needed labels Oct 8, 2026
@russhil
russhil force-pushed the feat/shader-barycentric-emulation branch from 1e08abb to dcbc6db Compare October 8, 2026 18:17
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 8, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 8, 2026
@russhil
russhil force-pushed the feat/shader-barycentric-emulation branch from dcbc6db to 2c80448 Compare October 9, 2026 06:58
russhil added a commit to russhil/AnyPS5 that referenced this pull request Oct 9, 2026
… stage

Without VK_KHR_fragment_shader_barycentric the recompiler dropped
v_interp_p1_f32 and made v_interp_p2_f32 read the attribute Vulkan
interpolated, but never wrote the I/J VGPRs: a pixel shader that read
them for anything else got 0, and v_interp_mov_f32 of P10/P20 (or P0 of
an interpolated input) needed PerVertexKHR and failed pipeline
validation.

The fallback translation now loads the I/J VGPRs and marks v_interp_p1
and v_interp_p2 with witnesses. After the front-end simplification
(SSA, folding, DCE, read-lane and masked-select elimination),
HostInterpolationChecker requires every p2 to read its J, and its p1's
I, from a barycentric of the parameter's kind, every p1 result to reach
only the matching p2 (through a write masked by the p2's EXEC, whose
inactive lanes keep the old register), no parameter read as raw
vertices and no slot shared by different interpolations. It then
lowers the witnesses to the host-interpolated attribute as before,
reruns DCE and masked-select elimination, and accepts the program only
when no barycentric read remains, so reusing the I/J VGPRs after
interpolating keeps host interpolation and the previous SPIR-V. Any other
program is translated with the barycentric path and marked as emulated:
its barycentrics become vec3 inputs (NoPerspective for the linear
weights) and its per-vertex parameters flat vec4[3] inputs, placed in
the free locations by LayoutBarycentricEmulation. The driver then
inserts a generated geometry stage between the vertex and fragment
shaders that copies the position and parameters, writes the one-hot
weights of each vertex and the three vertices' parameter values, so the
rasterizer interpolates the same weights the extension provides.

The stage needs geometryShader and a vertex shader before it;
tessellation, mesh and rect-list pipelines, other primitive types and
interfaces beyond 32 locations or the device's geometry limits throw.
CompiledShaderArtifact carries the emulation (shader disk cache format 21).

Hardware snaps window corners to 1/256 pixel before interpolating
(measured by yahya308 on an RTX 4070 Laptop and a Radeon 780M in boykopovar#1547);
lavapipe snaps only for coverage (lp_rast.h FIXED_ORDER 8), so the test
accepts either reference per pixel at the original 1e-4 margin.

Sources:
- v_interp_p1_f32/v_interp_p2_f32/v_interp_mov_f32 semantics (P0,
  P10, P20, I/J): AMD RDNA2 ISA, "Parameter Interpolation".
- BaryCoordKHR/BaryCoordNoPerspKHR weights and PerVertexKHR vertex order:
  SPV_KHR_fragment_shader_barycentric, VK_KHR_fragment_shader_barycentric.
- Geometry shader triangle input order and provoking vertex: Vulkan
  specification, "Primitive Topologies" and "Geometry Shading".
- Interpolation decorations decided by the fragment input: Vulkan
  specification, "Interpolation Decorations".
- lavapipe interpolation: Mesa src/gallium/drivers/llvmpipe/lp_rast.h
  (FIXED_ORDER 8), lp_setup_tri.c (subpixel_snap for coverage only),
  lp_state_setup.c (init_args, plane setup from float positions).
@russhil

russhil commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @yahya308, the hardware traces settled it. In 2c80448 (rebased on main f1451396) the test carries both references: BarycentricsSnapped() snaps the window corners to 1/256 before the edge functions, BarycentricsUnsnapped() is the previous math, and every interior I/J and p1/p2 pixel must match one of them within the original 1e-4. At (53, 36) channel 1 they give 0.237278 and 0.237381, your reading and lavapipe's. Lavapipe still passes (495/495). If you have time, could you re-run agc_driver_pixel_barycentric_emulation on the 4070 and the 780M to confirm it now passes within 1e-4 on both?

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 9, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 9, 2026
@adriadam10

Copy link
Copy Markdown
Collaborator

Checked on lavapipe (geometryShader, no barycentrics): agc_driver_graphics, agc_shader_disk_cache and agc_driver_pixel_barycentric_emulation pass with ANYPS5_REQUIRE_VULKAN=1, and the new tests don't build without the change.

The branch conflicted with main (tests/Graphics.cpp, tests/ShaderDiskCache.cpp, the static_assert sizes in ShaderDiskCache.cpp, TechnicalDebt.md). I rebased it and added one change: ShaderDiskCache::FormatVersion is 22, since main already took 21. Rebuilt and reran the scoped ctest (291/291) after the rebase.

Not checked here: behaviour on NVIDIA/Windows hardware. @yahya308, if you can, please rerun agc_driver_pixel_barycentric_emulation on the 4070 and the 780M with the current head.

@adriadam10
adriadam10 force-pushed the feat/shader-barycentric-emulation branch from 2c80448 to 886a704 Compare October 10, 2026 00:17
adriadam10 pushed a commit to russhil/AnyPS5 that referenced this pull request Oct 10, 2026
… stage

Without VK_KHR_fragment_shader_barycentric the recompiler dropped
v_interp_p1_f32 and made v_interp_p2_f32 read the attribute Vulkan
interpolated, but never wrote the I/J VGPRs: a pixel shader that read
them for anything else got 0, and v_interp_mov_f32 of P10/P20 (or P0 of
an interpolated input) needed PerVertexKHR and failed pipeline
validation.

The fallback translation now loads the I/J VGPRs and marks v_interp_p1
and v_interp_p2 with witnesses. After the front-end simplification
(SSA, folding, DCE, read-lane and masked-select elimination),
HostInterpolationChecker requires every p2 to read its J, and its p1's
I, from a barycentric of the parameter's kind, every p1 result to reach
only the matching p2 (through a write masked by the p2's EXEC, whose
inactive lanes keep the old register), no parameter read as raw
vertices and no slot shared by different interpolations. It then
lowers the witnesses to the host-interpolated attribute as before,
reruns DCE and masked-select elimination, and accepts the program only
when no barycentric read remains, so reusing the I/J VGPRs after
interpolating keeps host interpolation and the previous SPIR-V. Any other
program is translated with the barycentric path and marked as emulated:
its barycentrics become vec3 inputs (NoPerspective for the linear
weights) and its per-vertex parameters flat vec4[3] inputs, placed in
the free locations by LayoutBarycentricEmulation. The driver then
inserts a generated geometry stage between the vertex and fragment
shaders that copies the position and parameters, writes the one-hot
weights of each vertex and the three vertices' parameter values, so the
rasterizer interpolates the same weights the extension provides.

The stage needs geometryShader and a vertex shader before it;
tessellation, mesh and rect-list pipelines, other primitive types and
interfaces beyond 32 locations or the device's geometry limits throw.
CompiledShaderArtifact carries the emulation (shader disk cache format 21).

Hardware snaps window corners to 1/256 pixel before interpolating
(measured by yahya308 on an RTX 4070 Laptop and a Radeon 780M in boykopovar#1547);
lavapipe snaps only for coverage (lp_rast.h FIXED_ORDER 8), so the test
accepts either reference per pixel at the original 1e-4 margin.

Sources:
- v_interp_p1_f32/v_interp_p2_f32/v_interp_mov_f32 semantics (P0,
  P10, P20, I/J): AMD RDNA2 ISA, "Parameter Interpolation".
- BaryCoordKHR/BaryCoordNoPerspKHR weights and PerVertexKHR vertex order:
  SPV_KHR_fragment_shader_barycentric, VK_KHR_fragment_shader_barycentric.
- Geometry shader triangle input order and provoking vertex: Vulkan
  specification, "Primitive Topologies" and "Geometry Shading".
- Interpolation decorations decided by the fragment input: Vulkan
  specification, "Interpolation Decorations".
- lavapipe interpolation: Mesa src/gallium/drivers/llvmpipe/lp_rast.h
  (FIXED_ORDER 8), lp_setup_tri.c (subpixel_snap for coverage only),
  lp_state_setup.c (init_args, plane setup from float positions).
… stage

Without VK_KHR_fragment_shader_barycentric the recompiler dropped
v_interp_p1_f32 and made v_interp_p2_f32 read the attribute Vulkan
interpolated, but never wrote the I/J VGPRs: a pixel shader that read
them for anything else got 0, and v_interp_mov_f32 of P10/P20 (or P0 of
an interpolated input) needed PerVertexKHR and failed pipeline
validation.

The fallback translation now loads the I/J VGPRs and marks v_interp_p1
and v_interp_p2 with witnesses. After the front-end simplification
(SSA, folding, DCE, read-lane and masked-select elimination),
HostInterpolationChecker requires every p2 to read its J, and its p1's
I, from a barycentric of the parameter's kind, every p1 result to reach
only the matching p2 (through a write masked by the p2's EXEC, whose
inactive lanes keep the old register), no parameter read as raw
vertices and no slot shared by different interpolations. It then
lowers the witnesses to the host-interpolated attribute as before,
reruns DCE and masked-select elimination, and accepts the program only
when no barycentric read remains, so reusing the I/J VGPRs after
interpolating keeps host interpolation and the previous SPIR-V. Any other
program is translated with the barycentric path and marked as emulated:
its barycentrics become vec3 inputs (NoPerspective for the linear
weights) and its per-vertex parameters flat vec4[3] inputs, placed in
the free locations by LayoutBarycentricEmulation. The driver then
inserts a generated geometry stage between the vertex and fragment
shaders that copies the position and parameters, writes the one-hot
weights of each vertex and the three vertices' parameter values, so the
rasterizer interpolates the same weights the extension provides.

The stage needs geometryShader and a vertex shader before it;
tessellation, mesh and rect-list pipelines, other primitive types and
interfaces beyond 32 locations or the device's geometry limits throw.
CompiledShaderArtifact carries the emulation (shader disk cache format 21).

Hardware snaps window corners to 1/256 pixel before interpolating
(measured by yahya308 on an RTX 4070 Laptop and a Radeon 780M in boykopovar#1547);
lavapipe snaps only for coverage (lp_rast.h FIXED_ORDER 8), so the test
accepts either reference per pixel at the original 1e-4 margin.

Sources:
- v_interp_p1_f32/v_interp_p2_f32/v_interp_mov_f32 semantics (P0,
  P10, P20, I/J): AMD RDNA2 ISA, "Parameter Interpolation".
- BaryCoordKHR/BaryCoordNoPerspKHR weights and PerVertexKHR vertex order:
  SPV_KHR_fragment_shader_barycentric, VK_KHR_fragment_shader_barycentric.
- Geometry shader triangle input order and provoking vertex: Vulkan
  specification, "Primitive Topologies" and "Geometry Shading".
- Interpolation decorations decided by the fragment input: Vulkan
  specification, "Interpolation Decorations".
- lavapipe interpolation: Mesa src/gallium/drivers/llvmpipe/lp_rast.h
  (FIXED_ORDER 8), lp_setup_tri.c (subpixel_snap for coverage only),
  lp_state_setup.c (init_args, plane setup from float positions).
@adriadam10

Copy link
Copy Markdown
Collaborator

Checked on lavapipe (geometryShader, no barycentrics): agc_driver_graphics, agc_shader_disk_cache and agc_driver_pixel_barycentric_emulation pass with ANYPS5_REQUIRE_VULKAN=1, and the new tests don't build without the change.

The branch conflicted with main again (Pipeline.cpp, pipelineKey). I rebased it and added one change: ShaderDiskCache::FormatVersion is 23, since main already took 22. Rebuilt and reran the scoped ctest (294/294) after the rebase.

Not checked here: behaviour on NVIDIA/Windows hardware. @yahya308, if you can, please rerun agc_driver_pixel_barycentric_emulation on the 4070 and the 780M with the current head.

@adriadam10
adriadam10 force-pushed the feat/shader-barycentric-emulation branch from 886a704 to 4a2e605 Compare October 10, 2026 06:04
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 10, 2026
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
#822 core/shader/recompiler/ShaderDiskCache.hpp L15
#2369 core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp L21

@adriadam10
adriadam10 merged commit 3064754 into boykopovar:main Oct 10, 2026
12 checks passed
SleglJan pushed a commit to SleglJan/AnyPS5 that referenced this pull request Oct 10, 2026
… stage

Without VK_KHR_fragment_shader_barycentric the recompiler dropped
v_interp_p1_f32 and made v_interp_p2_f32 read the attribute Vulkan
interpolated, but never wrote the I/J VGPRs: a pixel shader that read
them for anything else got 0, and v_interp_mov_f32 of P10/P20 (or P0 of
an interpolated input) needed PerVertexKHR and failed pipeline
validation.

The fallback translation now loads the I/J VGPRs and marks v_interp_p1
and v_interp_p2 with witnesses. After the front-end simplification
(SSA, folding, DCE, read-lane and masked-select elimination),
HostInterpolationChecker requires every p2 to read its J, and its p1's
I, from a barycentric of the parameter's kind, every p1 result to reach
only the matching p2 (through a write masked by the p2's EXEC, whose
inactive lanes keep the old register), no parameter read as raw
vertices and no slot shared by different interpolations. It then
lowers the witnesses to the host-interpolated attribute as before,
reruns DCE and masked-select elimination, and accepts the program only
when no barycentric read remains, so reusing the I/J VGPRs after
interpolating keeps host interpolation and the previous SPIR-V. Any other
program is translated with the barycentric path and marked as emulated:
its barycentrics become vec3 inputs (NoPerspective for the linear
weights) and its per-vertex parameters flat vec4[3] inputs, placed in
the free locations by LayoutBarycentricEmulation. The driver then
inserts a generated geometry stage between the vertex and fragment
shaders that copies the position and parameters, writes the one-hot
weights of each vertex and the three vertices' parameter values, so the
rasterizer interpolates the same weights the extension provides.

The stage needs geometryShader and a vertex shader before it;
tessellation, mesh and rect-list pipelines, other primitive types and
interfaces beyond 32 locations or the device's geometry limits throw.
CompiledShaderArtifact carries the emulation (shader disk cache format 21).

Hardware snaps window corners to 1/256 pixel before interpolating
(measured by yahya308 on an RTX 4070 Laptop and a Radeon 780M in boykopovar#1547);
lavapipe snaps only for coverage (lp_rast.h FIXED_ORDER 8), so the test
accepts either reference per pixel at the original 1e-4 margin.

Sources:
- v_interp_p1_f32/v_interp_p2_f32/v_interp_mov_f32 semantics (P0,
  P10, P20, I/J): AMD RDNA2 ISA, "Parameter Interpolation".
- BaryCoordKHR/BaryCoordNoPerspKHR weights and PerVertexKHR vertex order:
  SPV_KHR_fragment_shader_barycentric, VK_KHR_fragment_shader_barycentric.
- Geometry shader triangle input order and provoking vertex: Vulkan
  specification, "Primitive Topologies" and "Geometry Shading".
- Interpolation decorations decided by the fragment input: Vulkan
  specification, "Interpolation Decorations".
- lavapipe interpolation: Mesa src/gallium/drivers/llvmpipe/lp_rast.h
  (FIXED_ORDER 8), lp_setup_tri.c (subpixel_snap for coverage only),
  lp_state_setup.c (init_args, plane setup from float positions).
SleglJan pushed a commit to SleglJan/AnyPS5 that referenced this pull request Oct 10, 2026
…ic-emulation

feat(shader): emulate fragment barycentrics with a generated geometry stage
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* build docs shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants