Skip to content

nvn_mtl: decode BC textures on CPU via launch-time function dispatch with RAM optimization - #891

Closed
romulof wants to merge 1 commit into
seedds:mainfrom
romulof:main
Closed

romulof wants to merge 1 commit into
seedds:mainfrom
romulof:main

Conversation

@romulof

@romulof romulof commented Nov 1, 2026 •

Copy link
Copy Markdown

What and why

Enables games using BC-compressed textures (such as The Legend of Zelda: Tears of the Kingdom, 0100F2C0115B6000) to run on GPUs without native BC compression (pre-A17 Pro iOS devices, such as iPhone 14 Pro / Apple A16 GPU) by decoding BC1–5, BC6H, and BC7 textures on the CPU into uncompressed Metal formats (RGBA8Unorm, RGBA8Unorm_sRGB, R8Unorm, RG8Unorm, RGBA16Float, or RGB9E5Float).

All BC texture operations are dispatched via specialized function pointers locked in at launch time across 3 concrete paths with zero runtime branch overhead, and includes an optional launch-time setting (DM_BC6H_RGB9E5) to halve the RAM footprint of HDR textures.

Symptom

When running The Legend of Zelda: Tears of the Kingdom (0100F2C0115B6000, v1.4.3) on an iPhone 14 Pro (Apple A16 GPU) with CPU BC texture decoding, the game boots and displays the "Nintendo Presents" intro screen, then aborts (SIGABRT) at frame 153 during texture view creation (ser=6059 / ser=6074):

[dm] [nvn-mtl] texture upgraded to allow format views ser=6059 base format 71 -> view format 131 (views=3)
_mtlValidateArgumentsForTextureViewOnDevice:1877: failed assertion `Texture Creation: texture view pixelFormat (MTLPixelFormatInvalid) not castable. source texture pixelFormat (MTLPixelFormatRGBA8Unorm_sRGB) not compatible with texture view pixelFormat (MTLPixelFormatInvalid).'

Earlier in the same frame (ser=2638, ser=2639, ser=2903), the log also reported [RED] errors when agl's GPU BC4 block encoder attempted to bind a CPU-decoded BC4 texture as a render target through an RGBA16UI (0x2c) view:

[dm] [nvn-mtl] texture upgraded to allow render-target use ser=2639 base format 10 -> view format 10 (upgrades=3)
[dm] [nvn-mtl] [RED] render target view format 0x2c cannot be attached (base texture Metal format 10 was created without the format-view usage): drawing in the base format
[dm] [nvn-mtl] [RED] failed to create render pipeline (vertex 354a54829ba273c9_b fragment 0544282c162305db_b): output of type uint4 is not compatible with a MTLPixelFormatR8Unorm color attachment.

Root cause

When running on a GPU without hardware BC texture compression (!g_dev.supportsBCTextureCompression), tex_desc creates BC1–BC7 / BC6H textures (0x41–0x50) in their unpacked Metal pixel format via nvn_bc_unpacked (RGBA8Unorm 70, RGBA8Unorm_sRGB 71, R8Unorm 10, RG8Unorm 30, RGBA16Float 115, or RGB9E5Float 113). However, several other paths in dm/gfx/nvn/nvn_mtl.m still looked up view formats through nvn_fmt_to_mtl or checked only mtl_fmt_block(base.pixelFormat):

  1. Texture views (tex_by_id): When the game registers a view_t with a BC view format (such as 0x45 RGB_sRGB_DXT1 or 0x46 RGBA_sRGB_DXT1 on a DXT1 texture whose base Metal texture was unpacked to RGBA8Unorm 70 or RGBA8Unorm_sRGB 71), tex_by_id called nvn_fmt_to_mtl(r.v.format, &pf, swz) directly. That returned MTLPixelFormatBC1_RGBA_sRGB (131), which is unsupported on A16 (MTLPixelFormatInvalid in Metal's validator) and not castable from RGBA8Unorm_sRGB (71), triggering tex_upgrade_pfv and aborting in [base newTextureViewWithPixelFormat:...].
  2. Non-sparse packaged texture uploads (pkg_upload_): Evaluating whether to decode on CPU occurred after the GPU compute deswizzle paths. Non-sparse BC textures therefore took the GPU block-deswizzle path (treating 4x4 texel blocks as 1x1 elements) into an unpacked RGBA8 texture instead of reaching the CPU nvn_bc_decode loop.
  3. Render target attachments (attach, rt_view_fmt): Because an unpacked BC4 (0x49) texture has base.pixelFormat == MTLPixelFormatR8Unorm (10), mtl_fmt_block(base.pixelFormat) was 0. When the game bound the BC4 texture as a render target with an RGBA16UI (0x2c) block-encoding view, attach upgraded the R8Unorm texture to MTLTextureUsageRenderTarget instead of treating nvn_fmt_bc(t->d.format) as a compressed texture and leaving the attachment unattached (a.texture = nil) when bcrt_attach does not apply.
  4. Buffer-to-texture and texture-to-buffer copies (copy_b2t, copy_t2b, mtl_read_texels): copy_b2t blitted raw BC blocks with compressed bytesPerRow directly into the unpacked RGBA8 texture without decoding via nvn_bc_decode. Similarly, copy_t2b and the GPU blit fallback in mtl_read_texels attempted to blit from an unpacked RGBA8 texture using compressed block row strides.
  5. On-chip tile read merge (fz_p2_draw) and clear_tex: Both resolved view.format via nvn_fmt_to_mtl without checking g_bc_cpu && nvn_bc_unpacked(...).

The change

  1. Always-enabled CPU BC decompression:

    • Decoding BC textures is treated as a device capability rather than a per-game setting. Removed tex_bc_cpu from game.tsv and game.py.
    • On GPUs without native BC compression (!supportsBCTextureCompression), CPU BC decompression is enabled automatically for all games.
    • Updated dm/ios/Sources/main.m (front_ios_device_check) so non-BC devices are no longer refused at boot.
  2. Launch-time function pointer dispatch (mtl_set_bc_path in nvn_mtl.m):

    • Eliminates runtime per-texture branch checks by binding specialized function pointers (g_pkg_upload, g_tex_desc, g_nvn_bc_unpacked, g_nvn_bc_decode) at launch time across 3 concrete paths:
      • Path 1 (BC_PATH_GPU): Hardware direct BC decompression when supportsBCTextureCompression is true. CPU scratch buffers and decoding logic are completely bypassed with zero runtime overhead.
      • Path 2 (BC_PATH_CPU_STANDARD): Standard CPU BC decompression. Decodes BC1–5 and BC7 into standard uncompressed Metal formats, and BC6H into RGBA16Float (8 bytes per pixel).
      • Path 3 (BC_PATH_CPU_RGB9E5): Reduced-RAM CPU BC decompression. Decodes BC6H HDR textures into RGB9E5Float (4 bytes per pixel) instead of RGBA16Float, halving memory consumption for HDR environment maps.
  3. RAM optimization setting (DM_BC6H_RGB9E5):

    • Added DM_BC6H_RGB9E5 toggle to dm/games/settings.tsv:
      • Label: "Sighly lower texture quality to save RAM"
      • Note: "Try this if game starts crashing in heavy scenes"
      • Hidden on devices with hardware BC support (supportsBCTextureCompression == true).
    • Added Swift UI launcher support in LauncherSettings.swift.
    • Documented in docs/reference/rendering.md, docs/settings.md, and docs/env_switches.tsv.
  4. Views, uploads, copies, and attachments handling:

    • tex_by_id, rt_view_fmt, attach, fz_p2_draw, and clear_tex: resolve BC view formats through g_nvn_bc_unpacked.
    • pkg_upload_: non-sparse uploads route cleanly through g_pkg_upload without GPU block-deswizzle interference.
    • attach: guards tex_upgrade and compressed-target checks using nvn_fmt_bc(t->d.format) so CPU-decoded BC textures are not upgraded to uncompressed render targets.
    • copy_b2t, copy_t2b, and mtl_read_texels: CPU decompression in copy_b2t before blitting, and skip raw block blits out of unpacked BC textures in copy_t2b and mtl_read_texels.

Checked

  • Tests:
    • clang -O2 ... t_bcdec.c nvn_bcdec.c -o /tmp/t_bcdec && /tmp/t_bcdec: BC1 matches independent DXT1 decoder, format map, edge trimming, BC4, BC6H RGB9E5 & RGBA16F - all OK.
    • make -C dm/gfx/tests check_t_view_swizzle GAME=01007EF00011E000 (MTL_DEBUG_LAYER=1 ./t_view_swizzle: 393 checks, 0 failed).
    • python3 dm/tools/tests/test_launcher_settings.py: Ran 15 tests, OK.
    • python3 dm/tools/env_switches.py --check: OK (328 switches, 0 without meaning).
    • python3 dm/tools/tests/test_docs.py: Ran 11 tests, OK.
  • Games run, with title ID and version:
    • The Legend of Zelda: Tears of the Kingdom (0100F2C0115B6000, v1.4.3): progresses past "Nintendo Presents" intro into title sequence and gameplay without crashing on iPhone 14 Pro (Apple A16 GPU, iOS 26.0).
  • Regression impact:
    • Devices with hardware BC (supportsBCTextureCompression == true) select Path 1 at launch time: all CPU decoding code and scratch buffers are bypassed completely with zero overhead.

Docs

  • The finding is in its game note or its docs/reference/<subsystem>.md entry, and python3 dm/tools/docs_symptoms.py was run (the symptom index)
  • No chain tool changed
  • No game files, keys or data extracted from a game in the change or the attachments

@romulof romulof changed the title nvn_mtl: decode BC textures on the CPU and handle BC views, uploads, copies and attachments on pre-A17 Pro GPUs nvn_mtl: decode BC textures on CPU via launch-time function dispatch with RAM optimization Nov 2, 2026
…nction pointers, and add RAM saving option

- CPU BC decompression is always enabled on GPUs without native BC support (no build-time or profile options needed)
- Eliminate runtime per-texture branch checks by binding specialized function pointers at launch time (g_nvn_bc_unpacked, g_nvn_bc_decode) across 3 concrete paths:
  1. Direct BC decompression via GPU (hardware BC)
  2. BC -> standard CPU decode (BC6H to RGBA16Float)
  3. BC -> RGB9E5 CPU decode (saves 50% RAM on BC6H HDR textures)
- Add launch option DM_BC6H_RGB9E5:
  Label: "Sighly lower texture quality to save RAM"
  Note: "Try this if game starts crashing in heavy scenes"
  Hidden on hardware BC GPUs, disabled by default.
- Document feature, RAM characteristics, and environment switches in project docs.
- Address clang-analyzer nullability, retain count, and scratch allocation findings in nvn_mtl and test suite.
@romulof

romulof commented Nov 2, 2026

Copy link
Copy Markdown
Author

The devices that do not support BC textures are the same ones lacking RAM (6GB, with 4GB available for apps, matching Switch total RAM), so would it make sense to always convert BC6H HDR textures into RGB9E5Float?

@romulof

romulof commented Nov 2, 2026

Copy link
Copy Markdown
Author

Closing this in line with upstream's policy from #488 to support only GPUs with native hardware BC sampling. We will continue maintaining this as a fork-only patch in NX2Apple.

@romulof romulof closed this Nov 2, 2026
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