Agent skill

forge-pipeline-texture-compression

Load GPU block-compressed textures (BC7/BC5) through the asset pipeline. Covers basisu encoding, UASTC transcoding, .ftex format, D3D12 alignment, and normal map channel handling.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/forge-pipeline-texture-compression

SKILL.md

forge-pipeline-texture-compression

Load GPU block-compressed textures through forge_scene.h. The asset pipeline compresses textures at build time (basisu → KTX2 → .ftex), and forge_scene_load_pipeline_texture() uploads the pre-transcoded blocks directly to the GPU — no runtime transcoding or mip generation.

Compression pipeline

bash
# Build basisu (once)
cmake --build build --target basisu

# Color textures → BC7 sRGB
basisu -uastc -ktx2 -mipmap -file basecolor.jpg
forge_texture_tool basecolor.ktx2 basecolor.ftex bc7_srgb

# Normal maps → BC5 (requires both flags)
basisu -uastc -ktx2 -mipmap -normal_map -separate_rg_to_color_alpha -file normal.jpg
forge_texture_tool normal.ktx2 normal.ftex bc5_unorm

# Linear textures (metallic-roughness, occlusion) → BC7 linear
basisu -uastc -ktx2 -mipmap -file metallic_roughness.jpg
forge_texture_tool metallic_roughness.ktx2 metallic_roughness.ftex bc7_unorm

Both tools are CPU-only — no GPU required. Works on headless servers.

Format selection

Texture type basisu flags .ftex format SDL GPU format
Base color (none) bc7_srgb BC7_RGBA_UNORM_SRGB
Emissive (none) bc7_srgb BC7_RGBA_UNORM_SRGB
Normal map -normal_map -separate_rg_to_color_alpha bc5_unorm BC5_RG_UNORM
Metallic-roughness (none) bc7_unorm BC7_RGBA_UNORM
Occlusion (none) bc7_unorm BC7_RGBA_UNORM

Normal map encoding — why both flags matter

-separate_rg_to_color_alpha: The Basis Universal BC5 transcoder reads channels 0 (R) and 3 (Alpha), not 0 (R) and 1 (G). This flag moves G into alpha during UASTC encoding so BC5 gets the correct data. Without it, the Y normal component is lost (all 255).

-normal_map: Treats input as linear data (not sRGB) and optimizes UASTC encoding for angular error rather than perceptual color error.

Sidecar detection

forge_scene_load_pipeline_texture() checks for a .meta.json sidecar next to each texture. If the sidecar has a compression block pointing to a .ftex file, the compressed path is used. Otherwise, it falls back to SDL_LoadSurface + GPU mipmaps.

Example .meta.json:

json
{
  "source": "basecolor.jpg",
  "output": "basecolor.jpg",
  "output_width": 2048,
  "output_height": 2048,
  "compression": {
    "codec": "uastc",
    "container": "ktx2",
    "compressed_file": "basecolor.ktx2",
    "ratio": 6.32,
    "normal_map": true
  }
}

D3D12 alignment requirement

D3D12 requires 256-byte row pitch alignment and 512-byte mip offset alignment for texture uploads. forge_scene_upload_compressed_texture() handles this by padding block rows and aligning offsets in the transfer buffer. Vulkan has no such requirement.

Small BC7/BC5 mips (≤32×32) have row pitches below 256 bytes. Without padding, SDL's D3D12 backend crashes non-deterministically.

Usage pattern

c
#define FORGE_PIPELINE_IMPLEMENTATION
#include "pipeline/forge_pipeline.h"

#define FORGE_SCENE_MODEL_SUPPORT
#define FORGE_SCENE_IMPLEMENTATION
#include "scene/forge_scene.h"

/* Load model — compressed textures are detected automatically */
ForgeSceneModel model;
forge_scene_load_model(&scene, &model,
                       "assets/Model/Model.fscene",
                       "assets/Model/Model.fmesh",
                       "assets/Model/Model.fmat",
                       "assets/Model");

/* Check compression stats */
SDL_Log("Textures: %u/%u compressed, VRAM: %.1f MB (%.1f MB uncompressed)",
        model.vram.compressed_texture_count, model.vram.total_texture_count,
        (float)model.vram.compressed_bytes / (1024.0f * 1024.0f),
        (float)model.vram.uncompressed_bytes / (1024.0f * 1024.0f));

.ftex binary format

text
Header (32 bytes):
  magic       u32    "FTEX" (0x58455446)
  version     u32    1
  format      u32    1=BC7_SRGB, 2=BC7_UNORM, 3=BC5_UNORM
  width       u32    base mip width
  height      u32    base mip height
  mip_count   u32    number of mip levels
  reserved    u32    0
  reserved    u32    0

Mip entries (16 bytes each × mip_count):
  offset      u32    byte offset from file start
  data_size   u32    compressed block data size
  width       u32    mip level width
  height      u32    mip level height

Block data:
  Contiguous compressed blocks, mip 0 first

Shader compatibility

The fragment shader reconstructs Z from RG for normal maps:

hlsl
float2 n_rg = normal_tex.Sample(normal_smp, uv).rg * 2.0 - 1.0;
float3 n = float3(n_rg, sqrt(saturate(1.0 - dot(n_rg, n_rg))));

This works for both BC5 (RG only) and RGBA8 (ignores B, recomputes Z).

Common mistakes

  • Missing -separate_rg_to_color_alpha for normal maps. BC5 transcoding reads R and Alpha channels, not R and G. Without this flag, the Y normal component is all 1.0 and lighting is completely wrong.
  • Missing -normal_map for normal maps. Without it, basisu treats the input as sRGB and optimizes for perceptual color error instead of angular error. Quality degrades.
  • Unaligned compressed texture uploads on D3D12. Small mips (≤32×32) have row pitches below 256 bytes. Pad block rows to 256-byte alignment and align mip offsets to 512 bytes, or SDL's D3D12 backend crashes.
  • Forgetting pixels_per_row after padding. If you pad the transfer buffer but don't set pixels_per_row on SDL_GPUTextureTransferInfo, SDL doesn't know the stride and reads garbage.
  • Using BC7 sRGB for linear data. Metallic-roughness and occlusion textures are linear — use bc7_unorm, not bc7_srgb.

Reference

  • GPU Lesson 42 — full lesson with debugging walkthrough
  • GPU Lesson 41 — model loading foundation
  • common/scene/forge_scene.hforge_scene_upload_compressed_texture()
  • common/pipeline/forge_pipeline.hforge_pipeline_load_ftex()
  • tools/texture/main.cforge_texture_tool (KTX2 → .ftex)

Expand your agent's capabilities with these related and highly-rated skills.

Didn't find tool you were looking for?

Be as detailed as possible for better results