Skip to content

feat(agc): sample the base level through unnormalized samplers - #1418

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
gjonn:feat/agc-unnormalized-implicit-lod
Oct 13, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
gjonn:feat/agc-unnormalized-implicit-lod

Conversation

@gjonn

@gjonn gjonn commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

What

An unnormalized sampler is refused when it is used by an implicit-LOD sample (in a pixel shader), a bias sample or a gradient sample. PPSA01317 stops there on current main (#4782):

DescriptorBindingBuilder::Populate unnormalized guest sampler is used by an implicit-LOD sample, which is not implemented

The hardware ignores the LOD for unnormalized coordinates and samples the base level. On single-level views, image_sample, _b, _cl, _d and _d_cl through an unnormalized S# return the same bits as image_sample_lz. Vulkan requires such samples to be explicit with LOD 0.

  • SamplesBaseLevelIfUnnormalized marks a sampler used only by explicit-LOD, implicit-LOD, bias and gradient samples, with no offsets, comparison or gathers.
  • DescriptorBindingBuilder::Prepare sets a pipeline specialization constant for each such sampler (UnnormalizedBaseLevelBase + sampler) from the unnormalized bit ProveUnnormalized already reads. ProveUnnormalized accepts these samplers.
  • EmitSampleOp branches on that constant. When it is set, the sample is OpImageSampleExplicitLod with LOD 0, and derivatives, bias, LOD and LOD clamp are dropped. When it is clear, the sample keeps its own form. Specialization removes the other branch.
  • Offsets, comparison, gathers and image_get_lod through unnormalized samplers are still refused. The driver still accepts these samples only on single-level views.

Tested

  • Windows 11, MinGW-w64 GCC 15.2.0, RTX 5090:
    • agc_driver_image_sample_unnormalized: image_sample_d with derivatives of 4.0 samples the base level through each of the five samplers. A 4-level view read by image_sample_d is still refused.
    • agc_shader_memory: an unnormalized pixel image_sample has no implicit-LOD sample, and a normalized one keeps it. An unnormalized image_sample_d drops its derivatives, and a normalized one keeps them in its own specialization of the same artifact.
    • Both tests fail on main with the refusal above. agc_shader_disk_cache and the other 47 agc_driver_image*, agc_driver_sampler*, agc_shader_*, agc_spirv_module and agc_driver_graphics tests pass.

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

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the behaviour matches what we measured on hardware: on single-level views image_sample, _b, _cl, _d and _d_cl through an unnormalized S# return the same bits as image_sample_lz.

The PR no longer applies to main, and the conflict is not mechanical:

  • ResourceSpecialization (ResourceMaterializer.hpp) was removed when shader compilation moved to registration (#639), so unnormalizedSamplers, its operator== and the BuildKey line have nowhere to go. ShaderDiskCache::FormatVersion is 25 on main (the PR bumps 7 → 8).
  • The unnormalized bit is already read from the sampler descriptor in DescriptorBindingBuilder.cpp (ProveUnnormalized, samplerUnnormalized in the physical binding). Please carry it to SamplerResource::unnormalized / EmitSampleOp from there instead of adding a second per-S# field to the specialization, and relax the unsupported mask in ProveUnnormalized as you do now.
  • Bump FormatVersion to one above main at the time you push (check open PRs for the same bump), and keep the serialization of the new SamplerResource field in encodeInfo/decodeInfo.
  • Rebase and rerun agc_driver_image_sample_unnormalized and shader_disk_cache on the merged tree; the first rebased push will also need fresh Linux CI.

The hardware ignores the LOD for unnormalized coordinates and samples the
base level. A sampler used only by explicit-LOD, implicit-LOD, bias and
gradient samples gets a pipeline specialization constant set from its
unnormalized bit when the binding plan is prepared. When the constant is
set, each of its samples is emitted as OpImageSampleExplicitLod with LOD 0,
which is what Vulkan requires of unnormalized samplers. Otherwise the
sample keeps its own form. Such shaders are no longer refused.
@gjonn
gjonn force-pushed the feat/agc-unnormalized-implicit-lod branch from 3d68c36 to 63ed0ff Compare October 13, 2026 02:59
@gjonn

gjonn commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

@adriadam10 Thanks. Rebased onto main (c13dc7e) and reworked.

The unnormalized bit can't reach EmitSampleOp as a SamplerResource field on main. The module is compiled at registration, before any S# is bound, and the bit is only known when DescriptorBindingBuilder::Prepare sees the snapshot. So this follows the texel-offset path from #822. Prepare sets a per-sampler specialization constant from the ProveUnnormalized result, and EmitSampleOp branches on it: LOD 0 when it is set, the sample's own form when it is clear. The constant is always supplied (0 or 1), so every normalized draw of a shader shares one specialized module, and that is the module registration warms. The unsupported mask in ProveUnnormalized is relaxed for these samplers.

Because of that, there is no new SamplerResource field and nothing new is serialized, so I left FormatVersion at 28. The cache key already includes the source version. I can bump it if you want one anyway.

Multi-level views are left as ac1c3b775 set them: the driver still accepts these samples only on single-level views, which is what you measured. The execution test checks that a 4-level view read by image_sample_d is still refused.

agc_driver_image_sample_unnormalized, agc_shader_memory and agc_shader_disk_cache pass on the merged tree, as do the other 47 image, sampler and shader tests. The first two fail on main. This push will need fresh Linux CI.

@dawid-swiecicki ported the same idea to a specialization constant in #4782. Thanks for confirming it gets PPSA01317 past this point.

@Tamalero

Copy link
Copy Markdown
Contributor

Another data point: High on Life (PPSA15388) stops on this error when loading a later save (the home base):

DescriptorBindingBuilder::Populate unnormalized guest sampler is used by an implicit-LOD sample, which is not implemented

With this PR cherry-picked onto my stack (main 3377986a plus #1077, #425 and #3202), it gets past that stop and reaches gameplay. #4487 is also needed for that. The agc_ suite has no new failures (Linux, RTX 4090). The next sampler stops in that area are listed on #4736.

Tested and written by Claude (AI assistant) on a contributor's machine.

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked the rework on top of current main: the per-sampler specialization constant set in DescriptorBindingBuilder::Prepare from ProveUnnormalized and the branch in EmitSampleOp (LOD 0 when set, own form otherwise) follow the texel-offset path, and no new serialized field is needed, so leaving FormatVersion is right. Hardware: on single-level views image_sample, _b, _cl, _d and _d_cl through an unnormalized S# match image_sample_lz bit for bit. agc_shader_memory, agc_shader_disk_cache, agc_driver_image_sample_unnormalized and the other image_sample_*/sampler tests pass on the merged tree; confirmed by @Tamalero on an RTX 4090 (High on Life reaches gameplay). Merging once the full Build is green.

@adriadam10
adriadam10 merged commit 8bcd97c into boykopovar:main Oct 13, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants