Skip to content

fix(vk): allow raw colour attachment usage and views on 2D BC textures on AMD - #2001

Open
1conver wants to merge 1 commit into
NotRexed:mainfrom
1conver:fix/amd-bc-raw-attachment-views
Open

1conver wants to merge 1 commit into
NotRexed:mainfrom
1conver:fix/amd-bc-raw-attachment-views

Conversation

@1conver

@1conver 1conver commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Resolves the issue where 2D block-compressed textures (such as BC6H/BC5 crystal emissive lights in Dodongo's Cavern) fail to receive colour attachment usage or valid views on AMD Windows drivers, resulting in skipped render passes and unwritten multicoloured/rainbow block artifacts. Closes #1801.

Why this is needed

  1. On AMD Windows Vulkan drivers (amdvlk), \�kGetPhysicalDeviceImageFormatProperties\ returns \VK_ERROR_FORMAT_NOT_SUPPORTED\ (-11) when queried with compressed format identifiers (\VK_FORMAT_BC6H_UFLOAT_BLOCK, \VK_FORMAT_BC5_UNORM_BLOCK) together with \VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT. Because line 1959 required
    == VK_SUCCESS\ for 2D images, \x.usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT\ was not set.
  2. Even with the usage flag set, \�iew_format_of(..., ViewUse::Attachment)\ previously discarded all BC formats with \if (use == ViewUse::Attachment && (is_astc(f) || is_bc(f))) return VK_FORMAT_UNDEFINED;. Because \VK_NULL_HANDLE\ was returned by \ exture_view(), \ argets()\ in \�k_draw.cpp:3735\ skipped adding the attachment (\if (!view) continue;), leaving the render pass unexecuted and the memory uninitialized (which decodes as saturated rainbow blocks in BC6H HDR).

Solution

  1. Grant \VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT\ for 2D and 3D BC textures when the underlying raw view format supports \VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT\ (line 1953). Picky drivers that strictly reject the image during \�kCreateImage\ still safely fall back via existing lines 2030-2033.
  2. In \�iew_format_of(), map BC attachment views through \storage_view_format(f)\ (\R32G32B32A32_UINT\ / \R32G32_UINT), allowing \ exture_view()\ to create the raw block-texel view as intended by \�k_draw.cpp:3742\ (\if (bc(tex->format) && !bc(f)) w = (w + 3) / 4, h = (h + 3) / 4;). Following feat(vk): GPU- and settings-independent pipeline state list (HKSTATE2) #2046 (HKSTATE2), \�ttachment_format()\ also uses \�iew_format_of(), ensuring pipeline prebuilding and \ argets()\ stay in complete agreement.

Passes \ ests/test_bc_raw_targets.py\ cleanly. Tested and confirmed on AMD Radeon RX 6600 XT.

@1conver
1conver marked this pull request as draft November 10, 2026 23:25
@1conver
1conver force-pushed the fix/amd-bc-raw-attachment-views branch from 95fc84a to 9e349dd Compare November 10, 2026 23:55
@1conver 1conver changed the title fix(vk): allow raw colour attachment usage on 2D BC textures on AMD fix(vk): allow raw colour attachment usage and views on 2D BC textures on AMD Nov 10, 2026
@1conver
1conver marked this pull request as ready for review November 10, 2026 23:57
@1conver
1conver force-pushed the fix/amd-bc-raw-attachment-views branch from 9e349dd to 183f2d5 Compare November 11, 2026 02:39
@1conver

1conver commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review @hckrman101!

1. Rebase & HKSTATE2 (view_format_of)

Rebased onto upstream/main (ceb8745). As requested, the attachment mapping is now placed in view_format_of():

    if (use == ViewUse::Storage) f = force_format != VK_FORMAT_UNDEFINED ? force_format : storage_view_format(f);
    if (use == ViewUse::Attachment && is_bc(f)) f = storage_view_format(f);
    if (use == ViewUse::Attachment && is_astc(f)) return VK_FORMAT_UNDEFINED;

Because attachment_format() also delegates to view_format_of(), the prebuilt pipeline state records (state_targets) match the exact raw attachment format (R32G32B32A32_UINT for 16-byte blocks, R32G32_UINT for 8-byte blocks) that targets() binds during draws.

2. Usage condition & AMD driver behavior

You were spot on about the redundant check inside if (format_props(raw).optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT).
We updated the condition to:

if (r == VK_SUCCESS || x.type == VK_IMAGE_TYPE_3D || x.type == VK_IMAGE_TYPE_2D)
    x.usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;

and updated the code comment to explain the AMD driver (amdvlk) behavior: queries for compressed formats with COLOR_ATTACHMENT_BIT return -11 (VK_ERROR_FORMAT_NOT_SUPPORTED) because the driver checks the base compressed format directly rather than raw block-texel views. But with EXTENDED_USAGE and BLOCK_TEXEL_VIEW_COMPATIBLE, vkCreateImage succeeds and allows attaching the raw view. If any driver were to strictly refuse the usage during vkCreateImage, the existing fallback at line 2030 (!((ci.usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) && is_bc(x.format)...)) catches it and retries without the flag.

3. Empirical In-Game Testing & Why Both Changes Are Strictly Required

We tested this directly in-game on an AMD Radeon RX 6600 XT (Adrenalin drivers on Windows) at the entrance of Dodongo's Cavern.

The first change alone did NOT fix the issue. Here is why:
In targets(NvnState& s) (gpu/vk/vk_draw.cpp:3735), Hookshot acquires the view for each attachment:

const VkImageView view = texture_view(tex, t, v, VK_IMAGE_VIEW_TYPE_2D, ViewUse::Attachment, &f);
if (!view) continue;

With only the usage bit on VkImage, view_format_of() returned VK_FORMAT_UNDEFINED, which led texture_view() to return VK_NULL_HANDLE.
Because view was null, targets() skipped adding the attachment (if (!view) continue;). This left at.n == 0 (or the color slot empty), causing the draw to skip the render pass entirely.

Because the emissive pass never executed into the target, the texture retained uninitialized VRAM memory from prior allocations, manifesting on AMD as solid, saturated rainbow diamond blocks on the crystals:

Dodongo's Cavern rainbow crystals before fix

When view_format_of() maps BC attachment views to storage_view_format(f):

  • texture_view() produces a valid block-texel view (R32G32B32A32_UINT).
  • targets() in vk_draw.cpp:3742 already contains logic specifically anticipating raw views of BC targets:
    const auto bc = [](VkFormat x) { return x >= VK_FORMAT_BC1_RGB_UNORM_BLOCK && x <= VK_FORMAT_BC7_SRGB_BLOCK; };
    if (bc(tex->format) && !bc(f)) w = (w + 3) / 4, h = (h + 3) / 4;
    This scales the render area to blocks matching the attachment view extent.
  • The render pass executes, correctly writing the emissive crystal data. Re-entering Dodongo's Cavern with both fixes in place rendered the crystals completely clean and artifact-free.

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This conflicts with main in gpu/vk/vk_texture.cpp, and the code it changes has been reworked. #2327 (device loss rendering to 3D BC slices) took 3D BC images off the raw-attachment path: they now render through a separate uncompressed mip, and the usage block at vk_texture.cpp:2043 is x.type != VK_IMAGE_TYPE_3D only. So the "3D: taken" half of this diff no longer applies.

Two points from the earlier review are still open after the rebase. First, || x.type == VK_IMAGE_TYPE_2D gives every 2D BC image the attachment usage on every vendor, whatever vkGetPhysicalDeviceImageFormatProperties says. That's outside the spec, and it changes NVIDIA and Intel too, not only amdvlk. Please scope it to the AMD driver where you saw -11, or show a validation-clean run. Second, the view_format_of change at line 159 attaches a BC-format view as R32G32B32A32_UINT, which the fragment shader writes float outputs into. The screenshot shows the crystals change, but not that the write is defined. A RenderDoc capture of the Dodongo's Cavern pass would settle it. The issue is now tracked in #1006, and it's listed as a regression since v2.0.12, so it's worth checking which commit caused it.

…s on AMD

On AMD drivers (amdvlk), vkGetPhysicalDeviceImageFormatProperties returns
VK_ERROR_FORMAT_NOT_SUPPORTED (-11) when querying compressed formats with
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, because the driver checks the compressed
format directly rather than raw block-texel views. But with EXTENDED_USAGE,
the driver creates the image and its raw views without error. Scope the usage
override to AMD (dev.props.vendorID == 0x1002) on 2D BC images when the raw
format supports it, relying on the existing vkCreateImage refusal fallback.
(3D BC attachments are handled via separate uncompressed mips per NotRexed#2327).

In addition, map BC formats under ViewUse::Attachment in view_format_of() to
their raw block-texel format (storage_view_format). Previously this returned
VK_FORMAT_UNDEFINED, causing texture_view() to return VK_NULL_HANDLE.
When drawing into BC targets, targets() skipped the attachment, leaving the
render pass unexecuted and the texture holding uninitialized VRAM memory
(rainbow diamond artifacts on Dodongo's Cavern crystals, NotRexed#1801). With HKSTATE2,
attachment_format() also uses view_format_of(), ensuring prebuilt pipeline
formats match what targets() binds.
@1conver
1conver force-pushed the fix/amd-bc-raw-attachment-views branch from 183f2d5 to 9b05cb6 Compare November 11, 2026 23:31
@1conver

1conver commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto the latest \upstream/main\ (\ ce53453) and addressed both points from @Joey-Magnus:

  1. Rebase & fix(vk): prevent device loss when rendering to 3D BC slices #2327 alignment:
    Resolved the conflict cleanly with fix(vk): prevent device loss when rendering to 3D BC slices #2327. Since fix(vk): prevent device loss when rendering to 3D BC slices #2327 migrated 3D BC attachments to separate uncompressed mips with copy-back, the 3D path here is left entirely untouched.

  2. Scoped 2D usage override to AMD:
    The override is now strictly scoped to AMD (\dev.props.vendorID == 0x1002):
    \\cpp
    const bool is_amd = dev.props.vendorID == 0x1002;
    if (x.type != VK_IMAGE_TYPE_3D && (format_props(raw).optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)) {
    const VkResult r = vkGetPhysicalDeviceImageFormatProperties(dev.physical, x.format, x.type, VK_IMAGE_TILING_OPTIMAL,
    x.usage | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, flags, &limits);
    if (r == VK_SUCCESS || is_amd) x.usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
    ...
    \
    On NVIDIA and Intel, the driver's query response (
    == VK_SUCCESS) is strictly obeyed without overrides. On AMD, where \�mdvlk\ returns -11\ on compressed format queries despite accepting raw block-texel attachment views via \EXTENDED_USAGE, the usage is granted (with the existing \�kCreateImage\ refusal retry fallback retained below if a driver were to fail creation).

  3. Format mapping in \�iew_format_of\ & v2.0.12 regression:
    Prior to HKSTATE2 (feat(vk): GPU- and settings-independent pipeline state list (HKSTATE2) #2046), \ exture_view\ did not discard BC under \ViewUse::Attachment\ (\if (use == ViewUse::Attachment && is_astc(f)) return VK_NULL_HANDLE;\ only checked ASTC). HKSTATE2 introduced \if (use == ViewUse::Attachment && (is_astc(f) || is_bc(f))) return VK_FORMAT_UNDEFINED;\ on the assumption that compressed formats are never targets. However, OoT does bind raw block views for BC targets (which \ argets()\ in \�k_draw.cpp:3842\ explicitly accommodates via \if (bc(tex->format) && !bc(f)) w = (w + 3) / 4, h = (h + 3) / 4;). Mapping \is_bc(f)\ to \storage_view_format(f)\ restores that raw view, allowing the render pass to execute and clear/write the crystal data.

Passes \ est_bc_raw_targets.py\ cleanly. Ready for another look @hckrman101!

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for scoping the usage override. The rebase onto #2327 is clean: the 3D path is untouched. I merged this into current main and ran the texture, BC, view and pipeline-state checks. All pass except nx2/test_bc_storage_views, which fails the same way on main on Linux.

Two points still stand:

  • gpu/vk/vk_texture.cpp:159: view_format_of now turns every BC-format attachment view into R32G32B32A32_UINT/R32G32_UINT on every vendor, not just AMD. attachment_format() uses the same function, so the pipeline state list changes for everyone too. The fragment shader still writes float outputs into a UINT attachment, which the spec leaves undefined. The screenshot shows the crystals change, but it doesn't show that the write is defined. Please scope this to the case you saw, or show a RenderDoc capture of the pass.
  • vk_texture.cpp:2064: vendorID == 0x1002 covers RADV, so the Steam Deck too, as well as AMD's Windows driver, not only amdvlk as the comment says. Has the -11 been seen on RADV? If not, please gate on driverID and run once with validation on.

#1006 lists this as a regression since v2.0.12. Finding the commit that caused it may give a smaller fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants