Arsitektur bindless rendering pada API grafis modern seperti DirectX 12 dan Vulkan memindahkan manajemen resource dari CPU binding table ke global descriptor heap berukuran besar. Shader membaca resource melalui indeks numerik mentah yang diteruskan via Push Constants atau Root Constants. Kesalahan offset atau dynamic index yang tidak tervalidasi dapat memicu out-of-bounds (OOB) access, page fault pada GPU, hingga Device Lost (TDR). Panduan ini membahas implementasi CI linting shader bindless untuk validasi resource descriptor index secara statis sebelum aset masuk ke build release.

Tantangan Validasi Statis pada Bindless Rendering

Pada pipeline grafis tradisional berbasis slot binding (misalnya DirectX 11), compiler seperti DXC atau FXC memverifikasi kesesuaian register (t0, s0) saat kompilasi. Sebaliknya, bindless rendering memperlakukan heap resource sebagai array tanpa batas tetap (unbounded array):

// HLSL SM 6.6 Bindless Model
Texture2D g_Textures[] : register(t0, space0);

struct PushData {
    uint materialTextureIndex;
    uint samplerIndex;
};
ConstantBuffer<PushData> PushConstants : register(b0, space1);

float4 PSMain(float2 uv : TEXCOORD) : SV_Target {
    // Risiko runtime crash jika index melebihi alokasi heap aplikasi
    return g_Textures[PushConstants.materialTextureIndex].Sample(g_Samplers[PushConstants.samplerIndex], uv);
}

Masalah utama yang harus dicegah pada level CI mencakup:

  • Ketidaksesuaian Struct Push Constant: Perbedaan ukuran struct data antara kode C++ host engine dan layout shader.
  • Batas Ukuran Array Unbounded: Batasan indeks statis yang melampaui alokasi maksimum descriptor heap target hardware (misalnya limit tier Vulkan atau D3D12 Tier 1/2 Resource Descriptor Heap).
  • Akses Uninitialized Index: Eksekusi jalur kode yang menggunakan register index sebelum diinisialisasi.

Arsitektur CI Pipeline untuk Linting Shader

Pipeline validasi CI berjalan dalam dua tahap utama: Validasi Binary/IR menggunakan tool bawaan vendor (dxc dan spirv-val), dilanjutkan dengan Static AST Inspection menggunakan tool kustom berbasis CLI yang membandingkan metadata refleksi terhadap kontrak pipeline C++.

[Shader Source (.hlsl / .vert)]
          |
     (dxc / glslangValidator)
          |
    [SPIR-V / DXIL IR]
          |------------------------------+
          |                              |
     [spirv-val]            [Custom Reflector Tool]
(Validasi Standard IR)     (Cek Binding & Push Constant)
          |                              |
          +--------------+---------------+ 
                         |
             [Deterministik Exit Code]
           (0 = Lolos, non-zero = Block)

1. Validasi Intermediate Representation (IR) dengan spirv-val

Eksekusi kompilasi shader ke format target SPIR-V atau DXIL, lalu jalankan linter validasi byte-code. Untuk Vulkan/SPIR-V, gunakan flag spesifik untuk menguji kepatuhan kemampuan descriptor indexing:

# Kompilasi HLSL ke SPIR-V dengan ekstensi descriptor indexing
dxc -T ps_6_6 -E PSMain shader.hlsl -spirv -fspv-target-env=vulkan1.3 -Fo shader.spv

# Validasi struktur SPIR-V
spirv-val --target-env vulkan1.3 shader.spv

2. Custom Static Checker: Validasi Root Signature dan Descriptor Bound

Compiler resmi tidak mengetahui konfigurasi memori runtime engine. Karena itu, CI memerlukan skrip CLI mandiri (misalnya ditulis dengan Python atau Rust) yang mengekstrak metadata refleksi shader menggunakan spirv-reflect atau dxc.exe -dumpbin -rootsig, lalu memverifikasinya terhadap kontrak engine (konstanta batas heap).

Berikut contoh skrip Python untuk memeriksa ukuran push constant block dan register space descriptor index:

#!/usr/bin/env python3
import sys
import json
import subprocess

MAX_ALLOWED_PUSH_CONSTANT_BYTES = 128
MAX_DESCRIPTOR_HEAP_INDEX = 1_000_000

def validate_shader(spv_path):
    # Ekstrak refleksi via spirv-cross ke format JSON
    cmd = ["spirv-cross", spv_path, "--reflect", "--output-type", "json"]
    res = subprocess.run(cmd, capture_output=True, text=True)
    
    if res.returncode != 0:
        print(f"[ERROR] Refleksi gagal untuk {spv_path}: {res.stderr}")
        return 1
        
    data = json.loads(res.stdout)
    
    # 1. Validasi Push Constant Size
    push_constants = data.get("push_constants", [])
    for pc in push_constants:
        size = pc.get("block_size", 0)
        if size > MAX_ALLOWED_PUSH_CONSTANT_BYTES:
            print(f"[VIOLATION] Push constant pada {spv_path} berukuran {size}B (Maksimum {MAX_ALLOWED_PUSH_CONSTANT_BYTES}B)")
            return 2
            
    # 2. Validasi Binding Model
    types = data.get("types", {})
    textures = data.get("sampled_images", [])
    for tex in textures:
        # Cek unbounded array definition ([])
        array_dims = tex.get("array", [])
        if not array_dims or array_dims[0] == 0:
            # Unbounded descriptor array terdeteksi
            continue
        if array_dims[0] > MAX_DESCRIPTOR_HEAP_INDEX:
            print(f"[VIOLATION] Array descriptor index statis melebihi heap limit: {array_dims[0]}")
            return 3

    print(f"[PASS] {spv_path} valid.")
    return 0

if __name__ == "__main__":
    if len(sys.argv) < 2:
        sys.exit(1)
    sys.exit(validate_shader(sys.argv[1]))

Menangani Dynamic Indexing dan False Positive

Pada bindless architecture, indeks resource sering ditentukan secara dinamis (misalnya instanceID % totalMaterials). Static analysis murni tidak bisa memprediksi runtime value. Jika CI menolak dynamic expressions, durasi build terhambat oleh penyesuaian false positive.

Gunakan kombinasi pendekatan berikut pada level kode dan linter:

  1. Software-Enforced Clamping: Terapkan macro guard pada shader source untuk memastikan indeks tidak pernah melompati batas memori heap target secara spekulatif:
// Clamp macro untuk mencegah akses illegal memori shader core
#define SAFE_BINDLESS_FETCH(Heap, Index, MaxLimit) \
    Heap[NonUniformResourceIndex(min(Index, MaxLimit - 1))]
  1. Lint Exemption Annotations: Sediakan parser atribut khusus (misalnya // [[ci::allow_dynamic_index]]) agar linter CI mengabaikan inspeksi batas statis pada baris yang telah diamankan dengan runtime branching logis.

Integrasi GitHub Actions Pipeline

Jalankan linting shader secara paralel sebelum tahap build binary engine. Pendekatan ini mempercepat siklus feedback jika terjadi breaking change pada shader interface.

name: Shader Bindless Lint Validation

on:
  pull_request:
    paths:
      - 'shaders/**'
      - 'engine/shaders/**'

jobs:
  lint-bindless-shaders:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Source Code
        uses: actions/checkout@v4

      - name: Install Vulkan SDK and SPIRV Tools
        run: |
          sudo apt-get update
          sudo apt-get install -y spirv-tools spirv-cross directx-shader-compiler

      - name: Compile and Validate Shaders
        run: |
          EXIT_CODE=0
          for shader in $(find shaders/ -name "*.hlsl"); do
            echo "Linting: $shader"
            
            # Kompilasi ke SPIR-V sementara
            spv_out="${shader}.spv"
            dxc -T ps_6_6 -E PSMain "$shader" -spirv -fspv-target-env=vulkan1.3 -Fo "$spv_out" || EXIT_CODE=1
            
            # Validasi struktur SPIR-V
            spirv-val --target-env vulkan1.3 "$spv_out" || EXIT_CODE=2
            
            # Validasi heap bounds and layout contracts
            python3 tools/ci/validate_bindless_layout.py "$spv_out" || EXIT_CODE=3
            
            rm -f "$spv_out"
          done
          
          exit $EXIT_CODE

Skema Exit Code Deterministik

Untuk mempermudah investigasi pipeline pada CI runner, definisikan skema exit code standar untuk seluruh tools validasi:

  • 0: Lolos validasi tanpa error.
  • 1: Kesalahan kompilasi dasar DXC (syntax error, unsupported target).
  • 2: Pelanggaran validasi SPIR-V (invalid memory model, malformed SSA structure via spirv-val).
  • 3: Pelanggaran kontrak layout bindless (push constant size mismatch, heap space index overflow).

Penerapan automated linting pada pipeline CI memastikan error invalid pointer reference dan out-of-bounds descriptor indexing tertangkap sebelum mencapai fase integrasi gameplay maupun release runtime.