fix(viewer): survive block-misaligned KTX2 textures on WebGPU (#467)
WebGPU rejects block-compressed textures whose base dimensions aren't multiples of 4; three's KTX2Loader only warns and transcodes anyway, so a baked GLB carrying one odd-sized user-item texture (e.g. 299x399) poisoned every render pass — endless 'Invalid BindGroup … Invalid CommandBuffer' spam. Route such payloads through a fallback transcode to uncompressed RGBA32 and repackage the pixels as a plain DataTexture (three's WebGPU upload path crashes on a CompressedTexture holding rgba8 data). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f6ffb93a39
commit
f6a58489b8
@@ -1,5 +1,112 @@
|
||||
import {
|
||||
type CompressedTexture,
|
||||
DataTexture,
|
||||
LinearFilter,
|
||||
LinearMipmapLinearFilter,
|
||||
RGBAFormat,
|
||||
UnsignedByteType,
|
||||
} from 'three'
|
||||
import { KTX2Loader } from 'three/examples/jsm/Addons.js'
|
||||
|
||||
/** The private KTX2Loader surface this module relies on (stable across three
|
||||
* releases but not part of the public types). */
|
||||
type KTX2LoaderInternals = {
|
||||
transcoderPath: string
|
||||
workerConfig: Record<string, boolean> | null
|
||||
_createTexture(buffer: ArrayBuffer, config?: Record<string, unknown>): Promise<unknown>
|
||||
}
|
||||
|
||||
/** Base width/height of a Basis-supercompressed KTX2 payload needing transcode
|
||||
* (vkFormat 0 = ETC1S/UASTC) whose dimensions aren't block-aligned, or null
|
||||
* for anything safe. Little-endian u32s: vkFormat at byte 12, pixelWidth at
|
||||
* 20, pixelHeight at 24. */
|
||||
function readMisalignedBasisSize(buffer: ArrayBuffer): { width: number; height: number } | null {
|
||||
if (buffer.byteLength < 28) return null
|
||||
const view = new DataView(buffer)
|
||||
const vkFormat = view.getUint32(12, true)
|
||||
if (vkFormat !== 0) return null
|
||||
const width = view.getUint32(20, true)
|
||||
const height = view.getUint32(24, true)
|
||||
return width % 4 !== 0 || height % 4 !== 0 ? { width, height } : null
|
||||
}
|
||||
|
||||
/**
|
||||
* KTX2Loader that survives block-misaligned textures. WebGPU rejects
|
||||
* block-compressed textures whose base dimensions aren't multiples of 4;
|
||||
* three's KTX2Loader only warns and transcodes anyway, and the resulting
|
||||
* invalid texture poisons every render pass that binds it (endless
|
||||
* "Invalid BindGroup … Invalid CommandBuffer" spam — baked GLBs carrying an
|
||||
* odd-sized user-item texture triggered exactly this). Such payloads are
|
||||
* routed to a fallback loader whose support flags report no compressed
|
||||
* formats, which makes the transcoder emit plain RGBA32 — uncompressed
|
||||
* textures have no alignment requirement.
|
||||
*/
|
||||
class AlignmentSafeKTX2Loader extends KTX2Loader {
|
||||
private rgbaFallback: KTX2Loader | null = null
|
||||
|
||||
private fallbackLoader(): KTX2LoaderInternals {
|
||||
if (!this.rgbaFallback) {
|
||||
this.rgbaFallback = new KTX2Loader(this.manager)
|
||||
this.rgbaFallback.setTranscoderPath((this as unknown as KTX2LoaderInternals).transcoderPath)
|
||||
// All formats unsupported → getTranscoderFormat falls through to RGBA32.
|
||||
;(this.rgbaFallback as unknown as KTX2LoaderInternals).workerConfig = {
|
||||
astcSupported: false,
|
||||
astcHDRSupported: false,
|
||||
etc1Supported: false,
|
||||
etc2Supported: false,
|
||||
dxtSupported: false,
|
||||
bptcSupported: false,
|
||||
pvrtcSupported: false,
|
||||
}
|
||||
}
|
||||
return this.rgbaFallback as unknown as KTX2LoaderInternals
|
||||
}
|
||||
|
||||
/** Both `load()` and GLTFLoader's KHR_texture_basisu path funnel through
|
||||
* this internal — overriding it covers every entry point. */
|
||||
async _createTexture(buffer: ArrayBuffer, config?: Record<string, unknown>): Promise<unknown> {
|
||||
const misaligned = readMisalignedBasisSize(buffer)
|
||||
if (misaligned) {
|
||||
console.warn(
|
||||
`[viewer] KTX2 texture is ${misaligned.width}x${misaligned.height} (not multiple-of-4); ` +
|
||||
'transcoding uncompressed so WebGPU can create it.',
|
||||
)
|
||||
const transcoded = (await this.fallbackLoader()._createTexture(
|
||||
buffer,
|
||||
config,
|
||||
)) as CompressedTexture
|
||||
// The RGBA32 result still arrives as a CompressedTexture; three's WebGPU
|
||||
// texture upload treats every CompressedTexture as block-compressed and
|
||||
// crashes looking up a block descriptor for rgba8. Repackage the decoded
|
||||
// pixels as a plain DataTexture instead.
|
||||
const mip = (
|
||||
transcoded.mipmaps as Array<{ data: Uint8Array; width: number; height: number }>
|
||||
)?.[0]
|
||||
if (!mip) return transcoded
|
||||
const texture = new DataTexture(mip.data, mip.width, mip.height, RGBAFormat, UnsignedByteType)
|
||||
texture.colorSpace = transcoded.colorSpace
|
||||
texture.generateMipmaps = true
|
||||
texture.minFilter = LinearMipmapLinearFilter
|
||||
texture.magFilter = LinearFilter
|
||||
texture.needsUpdate = true
|
||||
transcoded.dispose()
|
||||
return texture
|
||||
}
|
||||
return (KTX2Loader.prototype as unknown as KTX2LoaderInternals)._createTexture.call(
|
||||
this,
|
||||
buffer,
|
||||
config,
|
||||
)
|
||||
}
|
||||
|
||||
dispose(): this {
|
||||
this.rgbaFallback?.dispose()
|
||||
this.rgbaFallback = null
|
||||
super.dispose()
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Single shared KTX2 loader for the whole viewer — used both by the GLB loader
|
||||
* (`use-gltf-ktx2`) and by catalog finish textures (`materials.ts`). KTX2 must
|
||||
@@ -9,7 +116,7 @@ import { KTX2Loader } from 'three/examples/jsm/Addons.js'
|
||||
* from the viewer root the moment the renderer is ready (even when no GLB is in
|
||||
* the scene, so catalog `.ktx2` finishes still load).
|
||||
*/
|
||||
export const ktx2Loader = new KTX2Loader()
|
||||
export const ktx2Loader = new AlignmentSafeKTX2Loader()
|
||||
ktx2Loader.setTranscoderPath('https://cdn.jsdelivr.net/gh/pmndrs/drei-assets@master/basis/')
|
||||
|
||||
const configuredRenderers = new WeakSet<object>()
|
||||
|
||||
Reference in New Issue
Block a user