Agent skill

forge-pipeline-assets

Load and render .fmesh binary meshes and pipeline-processed textures in SDL GPU applications

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-assets-code-review-benchmar-coderabbit-prs2-forg

SKILL.md

Skill: Pipeline-Processed Assets

Load and render .fmesh binary meshes, .fmat material sidecars, and pipeline-processed textures in SDL GPU applications. This pattern replaces raw glTF loading with optimized binary assets produced by the forge-gpu asset pipeline.

When to use

  • Loading mesh assets processed by forge-mesh-tool (.fmesh v2 format)
  • Loading material data from .fmat sidecars (texture paths, PBR parameters)
  • Loading textures with .meta.json sidecar metadata
  • Setting up dual vertex formats (48-byte with tangents, 32-byte without)
  • Implementing LOD selection based on camera distance
  • Normal mapping with Gram-Schmidt TBN from pipeline tangent data

Key files

File Purpose
common/pipeline/forge_pipeline.h Runtime loader for .fmesh + .fmat + textures
tools/mesh/main.c CLI tool that generates .fmesh + .fmat from OBJ/glTF
lessons/gpu/39-pipeline-processed-assets/main.c Reference implementation

Key API calls

  • forge_pipeline_load_mesh(path, &mesh) — load .fmesh v2 binary into CPU struct
  • forge_pipeline_free_mesh(&mesh) — release CPU-side mesh data
  • forge_pipeline_has_tangents(&mesh) — check if mesh has 48-byte tangent vertices
  • forge_pipeline_lod_index_count(&mesh, lod) — get index count for a LOD level
  • forge_pipeline_lod_indices(&mesh, lod) — get index pointer for a LOD level
  • forge_pipeline_load_materials(path, &set) — load .fmat material sidecar
  • forge_pipeline_free_materials(&set) — release CPU-side material data
  • forge_pipeline_load_texture(path, &tex) — load texture via .meta.json sidecar
  • forge_pipeline_free_texture(&tex) — release CPU-side texture data

Pattern

1. Include the pipeline library

c
/* In exactly ONE .c file: */
#define FORGE_PIPELINE_IMPLEMENTATION
#include "pipeline/forge_pipeline.h"

2. Load a .fmesh mesh

c
ForgePipelineMesh mesh;
if (!forge_pipeline_load_mesh("model.fmesh", &mesh)) {
    /* handle error */
}

/* Check vertex format */
if (forge_pipeline_has_tangents(&mesh)) {
    /* 48-byte stride: position + normal + uv + tangent */
    ForgePipelineVertexTan *verts = (ForgePipelineVertexTan *)mesh.vertices;
} else {
    /* 32-byte stride: position + normal + uv */
    ForgePipelineVertex *verts = (ForgePipelineVertex *)mesh.vertices;
}

/* Upload to GPU */
SDL_GPUBuffer *vb = upload_gpu_buffer(device, SDL_GPU_BUFFERUSAGE_VERTEX,
    mesh.vertices, mesh.vertex_count * mesh.vertex_stride);

/* Access LOD levels */
for (uint32_t lod = 0; lod < mesh.lod_count; lod++) {
    uint32_t count = forge_pipeline_lod_index_count(&mesh, lod);
    const uint32_t *indices = forge_pipeline_lod_indices(&mesh, lod);
}

/* Cleanup */
forge_pipeline_free_mesh(&mesh);

3. Load materials from .fmat sidecar

c
ForgePipelineMaterialSet materials;
if (!forge_pipeline_load_materials("model.fmat", &materials)) {
    /* handle error */
}

/* Use material texture paths (not hardcoded filenames) */
for (uint32_t i = 0; i < materials.material_count; i++) {
    ForgePipelineMaterial *mat = &materials.materials[i];
    if (mat->base_color_texture[0]) {
        /* Extract filename, resolve in processed directory */
        const char *fname = filename_from_path(mat->base_color_texture);
        load_pipeline_texture(device, fname, /*srgb=*/true);
    }
    if (mat->normal_texture[0]) {
        const char *fname = filename_from_path(mat->normal_texture);
        load_pipeline_texture(device, fname, /*srgb=*/false);
    }
}

/* Cleanup */
forge_pipeline_free_materials(&materials);

4. Load a pipeline texture (via .meta.json sidecar)

c
ForgePipelineTexture tex;
if (!forge_pipeline_load_texture("diffuse.png", &tex)) {
    /* handle error — needs diffuse.meta.json sidecar */
}

/* Decode mip 0 (currently PNG bytes) */
SDL_IOStream *io = SDL_IOFromMem(tex.mips[0].data, tex.mips[0].size);
SDL_Surface *surface = SDL_LoadSurface_IO(io, false);
if (!surface) {
    /* decode failed — fall back to loading from disk */
    SDL_CloseIO(io);
    forge_pipeline_free_texture(&tex);
    surface = SDL_LoadSurface("diffuse.png");
} else {
    SDL_CloseIO(io);
    forge_pipeline_free_texture(&tex);
}

5. Generate .fmesh + .fmat files

bash
# From glTF with LOD levels and tangent generation
./build/tools/mesh/forge_mesh_tool \
    input.gltf output.fmesh \
    --lod-levels 1.0,0.5,0.25 --verbose

# Creates output.fmesh + output.fmat + output.meta.json

6. Create .meta.json sidecars for textures

json
{
  "width": 1024,
  "height": 1024,
  "format": "rgba8",
  "mips": [
    { "level": 0, "width": 1024, "height": 1024, "file": "texture.png" }
  ]
}

Vertex formats

Format Stride Fields Use case
No tangent 32 bytes pos(12) + normal(12) + uv(8) Basic Blinn-Phong
With tangent 48 bytes pos(12) + normal(12) + uv(8) + tangent(16) Normal mapping

LOD selection

c
uint32_t select_lod(float distance, uint32_t max_lods) {
    if (max_lods <= 1) return 0;
    if (distance < 3.0f) return 0;
    if (distance < 8.0f && max_lods > 1) return 1;
    if (max_lods > 2) return 2;
    return max_lods - 1;
}

Common mistakes

  • Forgetting FORGE_PIPELINE_IMPLEMENTATION — the header is header-only; exactly one .c file must define this macro before including it, or you get linker errors (undefined symbols for all forge_pipeline_* functions).
  • Using the wrong vertex stride for the pipeline.fmesh files with tangents have 48-byte vertices; without tangents, 32 bytes. Check forge_pipeline_has_tangents() and create separate GPU pipelines for each stride. SDL GPU validates stride at pipeline creation time.
  • Not creating separate shadow pipelines per stride — shadow shaders only read position (attribute 0), but the vertex stride still must match. You need one shadow pipeline per vertex stride (48-byte and 32-byte).
  • Loading textures without the .meta.json sidecarforge_pipeline_load_texture() reads <path>.meta.json alongside the texture file. If the sidecar is missing, the call fails.
  • Using SDL_LoadBMP_IO for PNG dataSDL_LoadBMP_IO only decodes BMP format. Use SDL_LoadSurface_IO (which auto-detects PNG and BMP) or fall back to loading the file directly from disk with SDL_LoadSurface().
  • Hardcoding texture filenames instead of loading .fmat — the .fmat sidecar maps each submesh to its material and lists texture paths. Use forge_pipeline_load_materials() and read base_color_texture, normal_texture, etc. from the material struct.
  • Forgetting to free pipeline dataforge_pipeline_load_mesh, forge_pipeline_load_materials, and forge_pipeline_load_texture allocate memory internally. Always call the matching _free function, even if you've already uploaded the data to the GPU.

Reference

  • Lesson 39 — Pipeline-Processed Assets
  • Asset Lesson 06 — Loading Processed Assets
  • Asset Lesson 07 — Materials

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