Skip to content

KTX2Loader: Fix ETC1S/UASTC prioritization#31871

Merged
Mugen87 merged 1 commit into
mrdoob:devfrom
zeux:ktx2-transcode-priority
Sep 17, 2025
Merged

KTX2Loader: Fix ETC1S/UASTC prioritization#31871
Mugen87 merged 1 commit into
mrdoob:devfrom
zeux:ktx2-transcode-priority

Conversation

@zeux
Copy link
Copy Markdown
Contributor

@zeux zeux commented Sep 10, 2025

Due to a historical accident, the list of formats for ETC1S was sorted using UASTC priority. This change fixes that.

However, this exposes a problem that already exists for UASTC to ETC1S as well: on Linux, Mesa drivers for various Intel and AMD GPUs expose support for ETC2 and ASTC extensions even if the hardware does not support them, as part of GLES compatibility. When a texture with an emulated format is uploaded, the driver runs a very expensive CPU side decompression; this runs on the main thread and causes performance and memory issues.

When using Chrome based browsers, ANGLE filters out ASTC and ETC extensions for us; we now detect Gecko based browsers like Firefox that don't use ANGLE and do this filtering ourselves.

In principle, it is possible for GPUs to support all formats - notably, Safari exposes all formats on macOS when using Apple Silicon hardware, as it genuinely supports all possible formats. In this case we still should prefer native (ASTC/ETC2) format targets as they are faster to transcode to. A corner case is a combination of Firefox / Asahi Linux on Apple Silicon hardware; in the future it might be possible to detect the unmasked vendor to disambiguate, but even that combination will simply use BC7 for UASTC or dual-slice ETC1S which is probably reasonable.

See #29730 (comment) for motivating profiles.

Fixes #29745

@zeux
Copy link
Copy Markdown
Contributor Author

zeux commented Sep 10, 2025

WebGL report for posterity; ETC2 and ASTC are emulated but you can only tell by observing a significant performance degradation when using these:

image

@mrdoob mrdoob requested a review from donmccurdy September 12, 2025 06:11
@mrdoob mrdoob added this to the r181 milestone Sep 12, 2025
Comment thread examples/jsm/loaders/KTX2Loader.js Outdated
Due to a historical accident, the list of formats for ETC1S was sorted
using UASTC priority. This change fixes that.

However, this exposes a problem that already exists for UASTC to ETC1S
as well: on Linux, Mesa drivers for various Intel and AMD GPUs expose
support for ETC2 and ASTC extensions even if the hardware does not
support them, as part of GLES compatibility. When a texture with an
emulated format is uploaded, the driver runs a very expensive CPU side
decompression; this runs on the main thread and causes performance and
memory issues.

When using Chrome based browsers, ANGLE filters out ASTC and ETC
extensions for us; we now detect Gecko based browsers like Firefox that
don't use ANGLE and do this filtering ourselves.

In principle, it is possible for GPUs to support all formats - notably,
Safari exposes all formats on macOS when using Apple Silicon hardware,
as it genuinely supports all possible formats. In this case we still
should prefer native (ASTC/ETC2) format targets as they are faster to
transcode to. A corner case is a combination of Firefox / Asahi Linux on
Apple Silicon hardware; in the future it might be possible to detect the
unmasked vendor to disambiguate, but even that combination will simply
use BC7 for UASTC or dual-slice ETC1S which is probably reasonable.
Copy link
Copy Markdown
Collaborator

@donmccurdy donmccurdy left a comment

Choose a reason for hiding this comment

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

Made a quick test at https://jsfiddle.net/donmccurdy/bkr3q8mc/ — prints navigatior.platform and whether Firefox is detected (true/false) — and as expected the Linux+Firefox checks return false on devices I have available. That's as expected, I don't have a Linux device currently.

Thank you @zeux!

@Mugen87 Mugen87 merged commit eac5acc into mrdoob:dev Sep 17, 2025
8 checks passed
@arpu
Copy link
Copy Markdown
Contributor

arpu commented May 31, 2026

@zeux hi zeus after this update ktx2 etc1 decode is very slow on my Linux Chrome

Display type                    : ANGLE_OPENGL
GL_VENDOR                       : Google Inc. (AMD)
GL_RENDERER                     : ANGLE (AMD, AMD Radeon RX 6500 XT (radeonsi navi24 ACO), OpenGL ES 3.2 Mesa 26.0.7)
GL_VERSION                      : OpenGL ES 3.0 (ANGLE 2.1.27515 git hash: a101e2d1db6d)
GL_EXTENSIONS                   : GL_AMD_performance_monitor GL_ANGLE_blob_cache GL_ANGLE_client_arrays GL_ANGLE_clip_cull_distance GL_ANGLE_compressed_texture_etc GL_ANGLE_depth_texture GL_ANGLE_framebuffer_blit GL_ANGLE_framebuffer_multisample GL_ANGLE_get_serialized_context_string GL_ANGLE_get_tex_level_parameter GL_ANGLE_instanced_arrays GL_ANGLE_memory_size GL_ANGLE_program_binary_readiness_query GL_ANGLE_program_cache_control GL_ANGLE_renderability_validation GL_ANGLE_request_extension GL_ANGLE_robust_client_memory GL_ANGLE_shader_pixel_local_storage GL_ANGLE_stencil_texturing GL_ANGLE_texture_compression_dxt3 GL_ANGLE_texture_compression_dxt5 GL_ANGLE_texture_external_update GL_ANGLE_texture_multisample GL_ANGLE_translated_shader_source GL_ARM_rgba8 GL_CHROMIUM_bind_generates_resource GL_CHROMIUM_bind_uniform_location GL_CHROMIUM_copy_texture GL_CHROMIUM_lose_context GL_EXT_blend_func_extended GL_EXT_blend_minmax GL_EXT_clear_texture GL_EXT_clip_control GL_EXT_clip_cull_distance GL_EXT_color_buffer_float GL_EXT_color_buffer_half_float GL_EXT_compressed_ETC1_RGB8_sub_texture GL_EXT_conservative_depth GL_EXT_debug_label GL_EXT_debug_marker GL_EXT_depth_clamp GL_EXT_discard_framebuffer GL_EXT_disjoint_timer_query GL_EXT_draw_buffers GL_EXT_draw_buffers_indexed GL_EXT_float_blend GL_EXT_frag_depth GL_EXT_instanced_arrays GL_EXT_map_buffer_range GL_EXT_memory_object GL_EXT_memory_object_fd GL_EXT_occlusion_query_boolean GL_EXT_polygon_offset_clamp GL_EXT_read_format_bgra GL_EXT_render_snorm GL_EXT_robustness GL_EXT_sRGB GL_EXT_sRGB_write_control GL_EXT_semaphore GL_EXT_semaphore_fd GL_EXT_shadow_samplers GL_EXT_texture_border_clamp GL_EXT_texture_compression_bptc GL_EXT_texture_compression_dxt1 GL_EXT_texture_compression_rgtc GL_EXT_texture_compression_s3tc_srgb GL_EXT_texture_cube_map_array GL_EXT_texture_filter_anisotropic GL_EXT_texture_format_BGRA8888 GL_EXT_texture_mirror_clamp_to_edge GL_EXT_texture_norm16 GL_EXT_texture_rg GL_EXT_texture_sRGB_R8 GL_EXT_texture_sRGB_RG8 GL_EXT_texture_sRGB_decode GL_EXT_texture_shadow_lod GL_EXT_texture_storage GL_EXT_texture_type_2_10_10_10_REV GL_EXT_unpack_subimage GL_KHR_blend_equation_advanced GL_KHR_blend_equation_advanced_coherent GL_KHR_debug GL_KHR_parallel_shader_compile GL_KHR_robustness GL_KHR_texture_compression_astc_ldr GL_KHR_texture_compression_astc_sliced_3d GL_MESA_framebuffer_flip_y GL_NV_depth_buffer_float2 GL_NV_fence GL_NV_framebuffer_blit GL_NV_pack_subimage GL_NV_pixel_buffer_object GL_NV_read_depth GL_NV_read_stencil GL_NV_shader_noperspective_interpolation GL_OES_EGL_image GL_OES_EGL_image_external GL_OES_EGL_image_external_essl3 GL_OES_EGL_sync GL_OES_compressed_EAC_R11_signed_texture GL_OES_compressed_EAC_R11_unsigned_texture GL_OES_compressed_EAC_RG11_signed_texture GL_OES_compressed_EAC_RG11_unsigned_texture GL_OES_compressed_ETC1_RGB8_texture GL_OES_compressed_ETC2_RGB8_texture GL_OES_compressed_ETC2_RGBA8_texture GL_OES_compressed_ETC2_punchthroughA_RGBA8_texture GL_OES_compressed_ETC2_punchthroughA_sRGB8_alpha_texture GL_OES_compressed_ETC2_sRGB8_alpha8_texture GL_OES_compressed_ETC2_sRGB8_texture GL_OES_depth24 GL_OES_draw_buffers_indexed GL_OES_element_index_uint GL_OES_fbo_render_mipmap GL_OES_get_program_binary GL_OES_mapbuffer GL_OES_packed_depth_stencil GL_OES_required_internalformat GL_OES_rgb8_rgba8 GL_OES_sample_variables GL_OES_shader_multisample_interpolation GL_OES_standard_derivatives GL_OES_surfaceless_context GL_OES_texture_3D GL_OES_texture_border_clamp GL_OES_texture_cube_map_array GL_OES_texture_float GL_OES_texture_float_linear GL_OES_texture_half_float GL_OES_texture_half_float_linear GL_OES_texture_npot GL_OES_texture_stencil8 GL_OES_texture_storage_multisample_2d_array GL_OES_vertex_array_object GL_WEBGL_video_texture

148.0.7778.215 (Offizieller Build) (64-Bit) Chrome
after patching the KTX2Loader with:

if (KTX2Loader.BasisWorker) {
  const originalWorkerStr = KTX2Loader.BasisWorker.toString();
  const patchedWorkerStr = originalWorkerStr.replace(
    /([a-zA-Z_$][a-zA-Z0-9_$]*)\.priorityETC1S\s*-\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\.priorityETC1S/g,
    '$1.priorityUASTC - $2.priorityUASTC'
  );
  KTX2Loader.BasisWorker.toString = function () {
    return patchedWorkerStr;
  };
}

than the decode is much faster

@zeux
Copy link
Copy Markdown
Contributor Author

zeux commented May 31, 2026

This sounds like on your version of Chrome, the same problem as I noted existed in Firefox, was happening too. In my testing this was not the case but perhaps Angle folks removed the workaround since.

You can try removing this part navigator.userAgent.indexOf( 'Firefox' ) >= 0 and seeing if the broad filtering fixes the problem then? If it does then maybe the false reporting of ETC support is now browser-wide on Linux and we'd need to apply this workaround regardless of the browser/user agent.

If that doesn't help, you'd need to find out which formats are supported and which are being selected for decoding to investigate this; I don't have AMD HW to test this at the moment. Specifically, logging all attributes of workerConfig would help, as well as a WebGL report for WebGL2 from https://webglreport.com/?v=2

@arpu
Copy link
Copy Markdown
Contributor

arpu commented May 31, 2026

webglreport:
Bildschirmfoto vom 2026-06-01 00-19-46

@zeux
Copy link
Copy Markdown
Contributor Author

zeux commented May 31, 2026

Yeah this report contains WEBGL_compressed_texture_etc which is not supported by this GPU, and since you're using Mesa drivers, they would do the same slow software decoding that happened in Firefox for me when this PR was created.

Does removing the Firefox user agent test from this patch fix the slowdown?

@arpu
Copy link
Copy Markdown
Contributor

arpu commented May 31, 2026

looks like i had to add this.workerConfig.etc1Supported = false;

works than with:

// Patch KTX2Loader.prototype.detectSupport to remove Firefox userAgent check and log workerConfig
if (KTX2Loader.prototype.detectSupport) {
  const originalDetectSupport = KTX2Loader.prototype.detectSupport;
  KTX2Loader.prototype.detectSupport = function (renderer) {
    // Call the original detectSupport method
    originalDetectSupport.call(this, renderer);

    // Apply the broad Linux emulation workaround (regardless of Firefox/browser)
    if (
      typeof navigator !== 'undefined' &&
      typeof navigator.platform !== 'undefined' &&
      navigator.platform.indexOf('Linux') >= 0 &&
      this.workerConfig &&
      this.workerConfig.astcSupported &&
      this.workerConfig.etc2Supported &&
      this.workerConfig.bptcSupported &&
      this.workerConfig.dxtSupported
    ) {
      // Disable formats that are likely to be emulated in software
      this.workerConfig.astcSupported = false;
      this.workerConfig.etc2Supported = false;
      this.workerConfig.etc1Supported = false;
    }

    // Diagnostic logging for workerConfig
    console.warn('THREE.KTX2Loader detectSupport (patched) result:', {
      platform: typeof navigator !== 'undefined' ? navigator.platform : 'undefined',
      userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'undefined',
      workerConfig: this.workerConfig
    });

    return this;
  };
}

// Patch KTX2Loader.prototype._createTextureFrom to log selected transcode format & type
if (KTX2Loader.prototype._createTextureFrom) {
  const originalCreateTextureFrom = KTX2Loader.prototype._createTextureFrom;
  KTX2Loader.prototype._createTextureFrom = function (transcodeResult, container) {
    const result = originalCreateTextureFrom.call(this, transcodeResult, container);

    if (transcodeResult && transcodeResult.data) {
      const { format, type } = transcodeResult.data;
      const formatName =
        Object.keys(KTX2Loader.EngineFormat).find(
          (key) => KTX2Loader.EngineFormat[key] === format
        ) || format;
      const typeName =
        Object.keys(KTX2Loader.EngineType).find(
          (key) => KTX2Loader.EngineType[key] === type
        ) || type;

      console.warn('THREE.KTX2Loader transcode (patched) selected format:', formatName, 'type:', typeName);
    }

    return result;
  };
}

installHook.js:1 THREE.KTX2Loader detectSupport (patched) result: 
{platform: 'Linux x86_64', userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36…KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36', workerConfig: {…}}
platform
: 
"Linux x86_64"
userAgent
: 
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36"
workerConfig
: 
astcHDRSupported
: 
false
astcSupported
: 
false
bptcSupported
: 
true
dxtSupported
: 
true
etc1Supported
: 
false
etc2Supported
: 
false
pvrtcSupported
: 
false
[[Prototype]]
: 
Object
[[Prototype]]
: 
Object

THREE.KTX2Loader transcode (patched) selected format: RGBA_BPTC_Format type: UnsignedByteType

@zeux
Copy link
Copy Markdown
Contributor Author

zeux commented May 31, 2026

Makes sense; I think in my case ETC1 wasn't marked as supported but in your case the browser reports both ETC1 & ETC2 as supported; neither are actually supported in hardware, and the ETC1S priority table says that we should try ETC2 & ETC1 before considering other formats.

Would you mind submitting the fix (without logging) as a pull request?

@arpu
Copy link
Copy Markdown
Contributor

arpu commented May 31, 2026

yes can do this tomorrow, but i wonder if this could have problemns on other plattforms too? or nvidia we should enable?

@zeux
Copy link
Copy Markdown
Contributor Author

zeux commented May 31, 2026

The code currently doesn't check the vendor; while in my case the vendor was also AMD, I know this also affects Intel. I'm not sure about NVidia but I think maybe the best approach is to adjust the code to not check the browser and to disable ETC1; the worst case scenario here is that we have a vendor (NV?) that reports both formats and actually supports them, but then we go through ETC1S => BC transcoding path instead of through ETC1S => ETC2 which is potentially a little faster?

I don't think it's too bad and we should probably err on the side of not running into a very expensive emulated software encoding here :) And this is Linux specific anyhow, so it's still narrowly scoped.

So I would suggest not adding vendor checks or other platform checks, until we get specific reports for anything else like a non-Linux system.

arpu added a commit to arpu/three.js that referenced this pull request Jun 1, 2026
Updated comments to clarify supported formats on Linux.after discussion mrdoob#31871 
@zeux
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.

KTX2Loader: Improve transcoder target format selection

5 participants