Skip to content

Transcode DDS BCn to ETC2 in case of missing hardware support for S3TC/BPTC - #7708

Open
Shivansps wants to merge 8 commits into
scp-fs2open:masterfrom
Shivansps:etc2-transcoding
Open

Shivansps wants to merge 8 commits into
scp-fs2open:masterfrom
Shivansps:etc2-transcoding

Conversation

@Shivansps

@Shivansps Shivansps commented Aug 15, 2026 •

Copy link
Copy Markdown
Contributor

Mobile GPUs might not have S3TC or BPTC, while it has become more common on Adreno 800s and Mali V5 Valhall you can never be sure, and support on older gen gpus is rare.
The objective of this PR is to do transcoding when the bitmap data is requested, if ETC2 support is present. Impact on loading times, specially for missions with a lot of ships and weapons are noticeable, but usable. Additionally a transcoding cache is added so you will only transcode a texture once, unless it is evicted from cache due to size limitation.

This process is about the same used by the bcn_layer that pc games emulators for android uses. And has been optimised for transcode speed and smallest vram usage over quality.

Current Features:

  1. ETC2 transcode on ddsutils.cpp in a similar way to how software decoding is implemented.
  2. etcpak lib, a fast transcoding lib, was added to do the runtime transcoding. (This lib supports both ETC2 and BC encoding)
  3. DXT3, DXT5 and BC7 are all converted to RGBA8_ETC2_EAC. DXT1 are converted to ETC2_RGB. etcpak do not support ETC2 RGBA1 what is the direct equivalent of DXT1. I have not observed any anormal result.
  4. a cmdline argument "-no_transcode_cache" to disable caching
  5. Fix Vulkan S3TC/BC7 detection logic, it was assuming to be always present what is not true for mobile gpus
  6. Textures to be transcoded to ETC2 are always downsized by 50% or limited to the max of 4096x4096.
  7. The fastest LZ4 compression is applied to the cache files, this has near no impact is transcode or read back times and reduces cache size by about 4 times... (2.3GB Icarus load uncompressed vs 536MB compressed).
  8. Added a configurable setting for the transcode cache size and a cache prune runs at the end of mission load.
  9. The cache system was made to support both etc2 and bcn. Its intended to be used in a future PR to transcode ETC2 to BCn in ktxutils if GPU do not support ETC2 (none of the desktop gpus in Vulkan mode supports it)

@dustinspace217 dustinspace217 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.

Ran this on desktop (no change, no cache files) and forced the transcode path on Mesa by hiding S3TC/BPTC: textures look right, and one SM1-01 load wrote 5,840 cache files (243 MB). A few things:

  1. On Vulkan this needs #7699: master's vulkan_preload has no ETC2 case, so a transcoded texture goes up as 32-bit BGRA and reads well past its buffer. Worth saying so in the description and keeping it draft until #7699 lands.
  2. The cache key only checks the first 64 KB. A DDS file's length is set by its size, format and mips, so a repainted texture keeps the same length and reuses the old transcode (across mods too, since the folder is shared). cf_chksum_long over the whole file, plus the etcpak revision in the tag, would cover it.
  3. With -portable_mode there's no user root, so the startup prune's cf_get_file_list passes an empty path into cf_get_list_of_files, which does path.back() on it. That runs on every portable start, even on desktop. Adding CF_LOCATION_ROOT_GAME like the shader cache does fixes it.
  4. The cap is only enforced at startup, so it gets passed after about a dozen mission loads. Pruning after writes, or touching the mtime on a hit, would help.
  5. Smaller:
    • bound stored_size to the payload size in the reader;
    • the mip halving also halves the reported size of 2D art, so maybe tie it to the existing texture-detail setting;
    • reuse the existing Vulkan format probe;
    • drop the unused ProcessDxtc files;
    • add CAPABILITY_ETC2 to gr_capabilities[].

@Shivansps

Shivansps commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor Author

-ProcessDxtc is keep because it will be used in ta future PR to add the inverse path of ETC2->BCn in ktxutils. There is no ETC2 support in desktop gpus, so someone making a mobile-first mod including ktx/etc2 textures will need something similar to this in ktxutils for vulkan. This is why the cache system is intended to be used for both. ill work on that one after this and #7699 are merged.

-Pruning while writting may cause additional problems, on top of deleting cache files that might be used in the current load. What i would want and It might be a good idea is to run it at the end of mission load, as the last step, but i dont know how to do it.

-"add CAPABILITY_ETC2 to gr_capabilities[]." where? i belive i already added to all relevant places.

-You are 100% sure the change in in the 2nd point is needed to avoid a cache colision? its easy to change, i priorised speed, i was worried that doing checksum over the entire file would slow down mission load even more.

-On the first point, yes, this is intended to go after #7699, i was not worried too much as the only real use is android devices and i expected both PRs to be in in the same stable release. And 7699 to be merged earlier.

@dustinspace217

Copy link
Copy Markdown
Contributor

Thanks.

  • ProcessDxtc: makes sense; a line in the description would keep it from reading as dead code.
  • Pruning: end of mission load is level_page_in() in freespace2/levelpaging.cpp. After bm_page_in_stop() every texture for the level is loaded, so tex_cache_prune() there won't remove anything the load still needs.
  • CAPABILITY_ETC2: it's in the enum and the gr_is_capable switches, but not the gr_capabilities[] table in graphics/2d.cpp (lines 66-86), which the log name and the shader preprocessor look up. One line next to S3TC covers it.
  • Cache key: not 100%. It needs the same name and length, and the 64 KB window only covers the top 32-64 rows of a 2048 texture, so a repaint lower down would hit. Instead of a full checksum, adding the file's write time from cf_find_file_location (loose-file mtime or the VP entry's timestamp) should cost almost nothing; a repack with no real change just re-transcodes once.
  • Going after Fix incomplete implementation of ETC2 textures in Vulkan #7699: fair. The portable-mode prune (point 3) still needs CF_LOCATION_ROOT_GAME, like the shader cache.

@Shivansps

Shivansps commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks.

  • ProcessDxtc: makes sense; a line in the description would keep it from reading as dead code.
  • Pruning: end of mission load is level_page_in() in freespace2/levelpaging.cpp. After bm_page_in_stop() every texture for the level is loaded, so tex_cache_prune() there won't remove anything the load still needs.
  • CAPABILITY_ETC2: it's in the enum and the gr_is_capable switches, but not the gr_capabilities[] table in graphics/2d.cpp (lines 66-86), which the log name and the shader preprocessor look up. One line next to S3TC covers it.
  • Cache key: not 100%. It needs the same name and length, and the 64 KB window only covers the top 32-64 rows of a 2048 texture, so a repaint lower down would hit. Instead of a full checksum, adding the file's write time from cf_find_file_location (loose-file mtime or the VP entry's timestamp) should cost almost nothing; a repack with no real change just re-transcodes once.
  • Going after Fix incomplete implementation of ETC2 textures in Vulkan #7699: fair. The portable-mode prune (point 3) still needs CF_LOCATION_ROOT_GAME, like the shader cache.

Allright, of the requested changes i did:

-Added CF_LOCATION_ROOT_GAME to cache location flags.
-Prune is now executed at the end of mission load, instead of at boot up.
-Added texture file modified date and etcpack revision in tag.
-stored_size bounds checking
-CAPABILITY_ETC2 ETC2 added to gr_capabilities[]:
-unifed vulkan probe

What i did not do:
-Change to the mipmaps, since software decompression does not do this, i dont exactly know how to properly do and test it. Maybe it can be left for a eventual improvement? It would have to implemented to software decompression as well.

Please check. Also remember i need to set the localization ids before merging on this one.

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.

2 participants