Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 170 additions & 1 deletion site/src/content/api/compressed-texture-format.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"subsystem": "rendering",
"importPath": "@codexo/exojs",
"tier": "stable",
"memberCount": 16,
"memberCount": 29,
"counts": {
"constructors": 0,
"methods": 0,
Expand All @@ -30,6 +30,84 @@
"id": "members",
"title": "Members",
"members": [
{
"name": "Astc10x10Unorm",
"signature": "Astc10x10Unorm",
"signatureTokens": [
{
"text": "Astc10x10Unorm",
"kind": "name"
}
],
"params": [],
"returnType": null,
"description": ""
},
{
"name": "Astc10x5Unorm",
"signature": "Astc10x5Unorm",
"signatureTokens": [
{
"text": "Astc10x5Unorm",
"kind": "name"
}
],
"params": [],
"returnType": null,
"description": ""
},
{
"name": "Astc10x6Unorm",
"signature": "Astc10x6Unorm",
"signatureTokens": [
{
"text": "Astc10x6Unorm",
"kind": "name"
}
],
"params": [],
"returnType": null,
"description": ""
},
{
"name": "Astc10x8Unorm",
"signature": "Astc10x8Unorm",
"signatureTokens": [
{
"text": "Astc10x8Unorm",
"kind": "name"
}
],
"params": [],
"returnType": null,
"description": ""
},
{
"name": "Astc12x10Unorm",
"signature": "Astc12x10Unorm",
"signatureTokens": [
{
"text": "Astc12x10Unorm",
"kind": "name"
}
],
"params": [],
"returnType": null,
"description": ""
},
{
"name": "Astc12x12Unorm",
"signature": "Astc12x12Unorm",
"signatureTokens": [
{
"text": "Astc12x12Unorm",
"kind": "name"
}
],
"params": [],
"returnType": null,
"description": ""
},
{
"name": "Astc4x4Unorm",
"signature": "Astc4x4Unorm",
Expand All @@ -43,6 +121,19 @@
"returnType": null,
"description": ""
},
{
"name": "Astc5x4Unorm",
"signature": "Astc5x4Unorm",
"signatureTokens": [
{
"text": "Astc5x4Unorm",
"kind": "name"
}
],
"params": [],
"returnType": null,
"description": ""
},
{
"name": "Astc5x5Unorm",
"signature": "Astc5x5Unorm",
Expand All @@ -56,6 +147,19 @@
"returnType": null,
"description": ""
},
{
"name": "Astc6x5Unorm",
"signature": "Astc6x5Unorm",
"signatureTokens": [
{
"text": "Astc6x5Unorm",
"kind": "name"
}
],
"params": [],
"returnType": null,
"description": ""
},
{
"name": "Astc6x6Unorm",
"signature": "Astc6x6Unorm",
Expand All @@ -69,6 +173,32 @@
"returnType": null,
"description": ""
},
{
"name": "Astc8x5Unorm",
"signature": "Astc8x5Unorm",
"signatureTokens": [
{
"text": "Astc8x5Unorm",
"kind": "name"
}
],
"params": [],
"returnType": null,
"description": ""
},
{
"name": "Astc8x6Unorm",
"signature": "Astc8x6Unorm",
"signatureTokens": [
{
"text": "Astc8x6Unorm",
"kind": "name"
}
],
"params": [],
"returnType": null,
"description": ""
},
{
"name": "Astc8x8Unorm",
"signature": "Astc8x8Unorm",
Expand Down Expand Up @@ -121,6 +251,19 @@
"returnType": null,
"description": ""
},
{
"name": "Bc4RSnorm",
"signature": "Bc4RSnorm",
"signatureTokens": [
{
"text": "Bc4RSnorm",
"kind": "name"
}
],
"params": [],
"returnType": null,
"description": ""
},
{
"name": "Bc4RUnorm",
"signature": "Bc4RUnorm",
Expand All @@ -134,6 +277,19 @@
"returnType": null,
"description": ""
},
{
"name": "Bc5RgSnorm",
"signature": "Bc5RgSnorm",
"signatureTokens": [
{
"text": "Bc5RgSnorm",
"kind": "name"
}
],
"params": [],
"returnType": null,
"description": ""
},
{
"name": "Bc5RgUnorm",
"signature": "Bc5RgUnorm",
Expand All @@ -147,6 +303,19 @@
"returnType": null,
"description": ""
},
{
"name": "Bc6hRgbFloat",
"signature": "Bc6hRgbFloat",
"signatureTokens": [
{
"text": "Bc6hRgbFloat",
"kind": "name"
}
],
"params": [],
"returnType": null,
"description": ""
},
{
"name": "Bc6hRgbUfloat",
"signature": "Bc6hRgbUfloat",
Expand Down
7 changes: 4 additions & 3 deletions site/src/content/api/texture-asset-type.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"title": "TextureAssetType",
"description": "GPU-ready Textures decoded from PNG, JPG, WebP, AVIF and GIF bytes, or from a KTX2 container holding a hardware-compressed payload. One type covers both because the payload kind is a property of the bytes, not of the asset: an AssetVariantSet rule may resolve one logical source to a compressed container where the device supports the format and to an image elsewhere, and a caller holding the handle sees a `Texture` either way.",
"description": "GPU-ready Textures decoded from PNG, JPG, WebP, AVIF, GIF, BMP and ICO bytes, or from a KTX2 container holding a hardware-compressed payload. One type covers both because the payload kind is a property of the bytes, not of the asset: an AssetVariantSet rule may resolve one logical source to a compressed container where the device supports the format and to an image elsewhere, and a caller holding the handle sees a `Texture` either way. An `.ico` file may hold several resolutions of one image. Which of them the decode yields is the browser's choice and is not specified anywhere, so an icon whose exact size matters should be shipped as the single image it is meant to be rather than as a multi-resolution container.",
"symbol": "TextureAssetType",
"kind": "class",
"subsystem": "assets",
Expand All @@ -19,8 +19,9 @@
"title": "Import",
"members": [],
"paragraphs": [
"GPU-ready Textures decoded from PNG, JPG, WebP, AVIF and GIF bytes, or from a KTX2 container holding a hardware-compressed payload.",
"One type covers both because the payload kind is a property of the bytes, not of the asset: an AssetVariantSet rule may resolve one logical source to a compressed container where the device supports the format and to an image elsewhere, and a caller holding the handle sees a `Texture` either way."
"GPU-ready Textures decoded from PNG, JPG, WebP, AVIF, GIF, BMP and ICO bytes, or from a KTX2 container holding a hardware-compressed payload.",
"One type covers both because the payload kind is a property of the bytes, not of the asset: an AssetVariantSet rule may resolve one logical source to a compressed container where the device supports the format and to an image elsewhere, and a caller holding the handle sees a `Texture` either way.",
"An `.ico` file may hold several resolutions of one image. Which of them the decode yields is the browser's choice and is not specified anywhere, so an icon whose exact size matters should be shipped as the single image it is meant to be rather than as a multi-resolution container."
],
"importLine": "import { TextureAssetType } from '@codexo/exojs'",
"sourceLink": null
Expand Down
4 changes: 3 additions & 1 deletion site/src/content/guide/assets/device-variants.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ Two things do not apply to a compressed payload, and ignoring them is not a simp
- **`generateMipMap`** cannot derive a mip level from compressed blocks. Compress the chain level by level and ship it inside the container; a file with one level samples without mips however the sampler is configured.

<Callout type="warning" title="Universal formats are not decoded">
A BasisLZ/ETC1S or UASTC "universal" KTX2 file needs a transcoder, which the engine does not carry, and loading one fails with an `AssetDecodeError` naming the scheme. Encode one file per target format instead - `toktx --target_type RGBA --encode uastc` and its per-format equivalents - and let a variant rule choose. Zstandard- and ZLIB-supercompressed files are rejected the same way.
A BasisLZ/ETC1S or UASTC "universal" KTX2 file needs a transcoder, which the engine does not carry, and loading one fails with an `AssetDecodeError` naming the scheme. Encode one file per target format instead - `toktx --target_type RGBA --encode uastc` and its per-format equivalents - and let a variant rule choose. Zstandard-supercompressed files are rejected the same way.

ZLIB supercompression (KTX2 scheme 3) does load: it is a plain zlib stream per mip level, which the browser inflates itself, so it costs no decoder and no bundle size. It saves download bytes on top of the block compression and is unpacked before upload - the texture in VRAM is the same either way.
</Callout>

## Constructing one directly
Expand Down
6 changes: 4 additions & 2 deletions site/src/content/guide/rendering/render-targets.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ const ids = gbuffer.attachment(1);

Each attachment is an ordinary `RenderTexture` and is sampled like any other texture afterwards. The target **owns** them: it creates them, resizes them with itself, and destroys them with itself. Read `app.backend.maxColorAttachments` for the ceiling on the current device.

<Callout type="warning" title="Only a custom mesh material can write one">
A fragment shader has to declare one output per attachment. Sprites, text, nine-slice and repeating sprites, video, and the default mesh material all declare exactly one, so drawing any of them into a multi-attachment target throws — as does alpha-mask or backdrop-blend compositing. Give the geometry a `MeshMaterial` whose shader writes every slot:
<Callout type="warning" title="Only a custom material can write one">
A fragment shader has to declare one output per attachment. Text, nine-slice and repeating sprites, video, and both default materials declare exactly one, so drawing any of them into a multi-attachment target throws — as does alpha-mask or backdrop-blend compositing. Give the drawable a `MeshMaterial` or a `SpriteMaterial` whose shader writes every slot:

```wgsl
struct FragmentOut {
Expand All @@ -149,6 +149,8 @@ fn fragmentMain(input: VertexOutput) -> FragmentOut {
```

The GLSL counterpart declares `layout(location = 0) out vec4 outColor;` and `layout(location = 1) out vec4 outId;`.

A `SpriteMaterial` takes the same fragment shader, which is what a 2D G-buffer usually wants: the scene is already authored as sprites, so it needs no mesh geometry to write a normal or id buffer.
</Callout>

If one pass only ever produces one image, a plain `RenderTexture` is the right tool — this exists for the case where it genuinely produces two.
Expand Down
7 changes: 5 additions & 2 deletions src/assets/factories/TextureFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Texture } from '#rendering/texture/Texture';
import type { SamplerOptions, TextureOptions } from '#rendering/texture/TextureOptions';

import { decodeImageBlob } from './decodeImageBlob';
import { isKtx2, parseKtx2 } from './ktx2';
import { inflateKtx2Levels, isKtx2, parseKtx2 } from './ktx2';
import { ObjectUrlPool } from './ObjectUrlPool';

/** Options accepted by an asset of the built-in `texture` type. */
Expand Down Expand Up @@ -50,7 +50,10 @@ export class TextureFactory implements AssetFactory<ArrayBuffer, Texture, Textur
}

private async _createFromKtx2(source: ArrayBuffer, name: string, textureOptions: Partial<TextureOptions> | undefined): Promise<Texture> {
const payload = parseKtx2(source, name);
// ZLIB supercompression is inflated ahead of the parser rather than inside
// it: DecompressionStream is a stream, and keeping the parser synchronous
// keeps it testable without I/O.
const payload = parseKtx2(await inflateKtx2Levels(source, name), name);

if (payload.kind === 'compressed') {
// Copied key by key rather than picked with a destructure: a key present
Expand Down
Loading
Loading