Initial release

This commit is contained in:
civ
2026-08-16 18:33:03 +07:00
commit 7ade4e1152
1966 changed files with 412966 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
# Numerical validation of the SS-flow DiT forward pass against a PyTorch
# reference (generate the reference with tests/ref_ss_flow.py).
add_executable(test_ss_flow_forward test_ss_flow_forward.cpp)
target_link_libraries(test_ss_flow_forward PRIVATE trellis2)
target_compile_features(test_ss_flow_forward PRIVATE cxx_std_14)
# Full flow-Euler sampling loop vs the PyTorch reference (tests/ref_ss_sample.py).
add_executable(test_ss_sample test_ss_sample.cpp)
target_link_libraries(test_ss_sample PRIVATE trellis2)
target_compile_features(test_ss_sample PRIVATE cxx_std_14)
# SS decoder forward vs the PyTorch reference (tests/ref_ss_dec.py).
add_executable(test_ss_dec test_ss_dec.cpp)
target_link_libraries(test_ss_dec PRIVATE trellis2)
target_compile_features(test_ss_dec PRIVATE cxx_std_14)
# PIL-compatible preprocessing byte-exactness vs the Python reference.
add_executable(test_preprocess test_preprocess.cpp)
target_link_libraries(test_preprocess PRIVATE trellis2)
target_include_directories(test_preprocess PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../stb)
target_compile_features(test_preprocess PRIVATE cxx_std_14)
# Deterministic border-connected black/white background removal (no models).
add_executable(test_background_removal test_background_removal.cpp)
target_link_libraries(test_background_removal PRIVATE trellis2)
target_compile_features(test_background_removal PRIVATE cxx_std_14)
# DINOv3 encoder per-layer parity vs the PyTorch reference
# (scripts/dump_dino_reference.py -> dumps/reference_dino.gguf).
add_executable(test_dino test_dino.cpp)
target_link_libraries(test_dino PRIVATE trellis2)
target_compile_features(test_dino PRIVATE cxx_std_14)
# Shape-SLAT flow + FDG decoder parity vs the PyTorch reference
# (scripts/dump_slat_reference.py -> dumps/reference_slat.gguf).
add_executable(test_slat test_slat.cpp)
target_link_libraries(test_slat PRIVATE trellis2)
target_compile_features(test_slat PRIVATE cxx_std_14)
# 1024_cascade HR-stage parity vs the PyTorch reference
# (scripts/dump_cascade_reference.py -> dumps/reference_cascade.gguf).
add_executable(test_cascade test_cascade.cpp)
target_link_libraries(test_cascade PRIVATE trellis2)
target_compile_features(test_cascade PRIVATE cxx_std_14)
# PBR-texture stage parity vs the PyTorch reference
# (scripts/dump_texture_reference.py -> dumps/reference_texture.gguf).
add_executable(test_texture test_texture.cpp)
target_link_libraries(test_texture PRIVATE trellis2)
target_compile_features(test_texture PRIVATE cxx_std_14)
# Self-contained isosurface extractor (examples/marching_cubes.h) on analytic
# fields — watertight-manifold + Euler-characteristic invariants. No model.
add_executable(test_marching_cubes test_marching_cubes.cpp)
target_include_directories(test_marching_cubes PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../examples)
target_compile_features(test_marching_cubes PRIVATE cxx_std_14)
# Sparse PBR-volume trilinear sampling used by the integrated texture path.
add_executable(test_pbr_sampling test_pbr_sampling.cpp)
target_include_directories(test_pbr_sampling PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..)
target_compile_features(test_pbr_sampling PRIVATE cxx_std_14)
# CPU GLB bake smoke/regression test, including six-channel alpha preservation.
add_executable(test_mesh_export test_mesh_export.cpp)
target_link_libraries(test_mesh_export PRIVATE trellis2)
target_compile_features(test_mesh_export PRIVATE cxx_std_14)
if(TRELLIS2_HAVE_CGAL)
add_executable(test_print_remesh test_print_remesh.cpp)
target_link_libraries(test_print_remesh PRIVATE trellis2)
target_compile_features(test_print_remesh PRIVATE cxx_std_14)
endif()
# ── ctest registration ────────────────────────────────────────────────────────
# Asset paths default to the in-repo locations produced by
# scripts/download_models.sh + converters + scripts/refgen.sh, overridable via
# TRELLIS2_GGUF_DIR / TRELLIS2_DUMPS. Tests exit 77 when assets are absent, so
# a fresh checkout still gets a green (skipped) suite.
set(T2_GGUFS "$ENV{TRELLIS2_GGUF_DIR}")
if(NOT T2_GGUFS)
set(T2_GGUFS "${CMAKE_CURRENT_SOURCE_DIR}/../ggufs")
endif()
set(T2_DUMPS "$ENV{TRELLIS2_DUMPS}")
if(NOT T2_DUMPS)
set(T2_DUMPS "${CMAKE_CURRENT_SOURCE_DIR}/../dumps")
endif()
set(T2_TESTS "${CMAKE_CURRENT_SOURCE_DIR}")
add_test(NAME marching_cubes COMMAND test_marching_cubes)
add_test(NAME pbr_sampling COMMAND test_pbr_sampling)
add_test(NAME mesh_export COMMAND test_mesh_export)
if(TRELLIS2_HAVE_CGAL)
add_test(NAME print_remesh COMMAND test_print_remesh)
endif()
add_test(NAME background_removal COMMAND test_background_removal)
add_test(NAME preprocess COMMAND test_preprocess
"${T2_DUMPS}/fixture_rgba.png" "${T2_DUMPS}/fixture_512.png")
set_tests_properties(preprocess PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME dino COMMAND test_dino
"${T2_GGUFS}/dino_f32.gguf" "${T2_DUMPS}/reference_dino.gguf")
set_tests_properties(dino PROPERTIES SKIP_RETURN_CODE 77 LABELS "model")
add_test(NAME ss_flow_forward COMMAND test_ss_flow_forward
"${T2_GGUFS}/ss_flow_f32.gguf" "${T2_TESTS}/ss_flow_ref.bin")
set_tests_properties(ss_flow_forward PROPERTIES SKIP_RETURN_CODE 77 LABELS "model")
add_test(NAME ss_sample COMMAND test_ss_sample
"${T2_GGUFS}/ss_flow_f32.gguf" "${T2_TESTS}/ss_sample_ref.bin")
set_tests_properties(ss_sample PROPERTIES SKIP_RETURN_CODE 77 LABELS "model;slow")
add_test(NAME ss_dec COMMAND test_ss_dec
"${T2_GGUFS}/ss_dec_f32.gguf" "${T2_TESTS}/ss_dec_ref.bin")
set_tests_properties(ss_dec PROPERTIES SKIP_RETURN_CODE 77 LABELS "model")
add_test(NAME slat COMMAND test_slat
"${T2_GGUFS}/slat_flow_f32.gguf" "${T2_GGUFS}/shape_dec_f32.gguf"
"${T2_DUMPS}/reference_slat.gguf")
set_tests_properties(slat PROPERTIES SKIP_RETURN_CODE 77 LABELS "model;slow"
ENVIRONMENT "TRELLIS2_DINODATA=${T2_DUMPS}/fixture.dinodata")
add_test(NAME cascade COMMAND test_cascade
"${T2_GGUFS}/slat_flow_f32.gguf" "${T2_GGUFS}/slat_flow_1024_f32.gguf"
"${T2_GGUFS}/shape_dec_f32.gguf" "${T2_DUMPS}/reference_cascade.gguf")
set_tests_properties(cascade PROPERTIES SKIP_RETURN_CODE 77 LABELS "model;slow")
add_test(NAME texture COMMAND test_texture
"${T2_GGUFS}/shape_enc_f16.gguf" "${T2_GGUFS}/tex_slat_flow_512_f16.gguf"
"${T2_GGUFS}/tex_dec_f16.gguf" "${T2_DUMPS}/reference_texture.gguf")
set_tests_properties(texture PROPERTIES SKIP_RETURN_CODE 77 LABELS "model;slow")
+92
View File
@@ -0,0 +1,92 @@
// Helpers for comparing C++ activations against PyTorch reference dumps
// (dumps/reference_*.gguf written by scripts/dump_*_reference.py).
//
// Same approach as depth-anything.cpp's tests/parity.hpp: the dump format is
// GGUF itself, tensors are flat f32 in the reference's row-major order, and
// the gate is elementwise |got - ref| <= atol + rtol * |ref|.
#pragma once
#include "ggml.h"
#include "gguf.h"
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
namespace t2_parity {
struct baseline {
gguf_context * gguf = nullptr;
ggml_context * ctx = nullptr;
bool open(const std::string & path) {
gguf_init_params p;
p.no_alloc = false; // load tensor data into ctx
p.ctx = &ctx;
gguf = gguf_init_from_file(path.c_str(), p);
return gguf != nullptr;
}
bool load(const std::string & name, std::vector<float> & out) const {
ggml_tensor * t = ggml_get_tensor(ctx, name.c_str());
if (!t || t->type != GGML_TYPE_F32) return false;
out.resize((size_t) ggml_nelements(t));
std::memcpy(out.data(), t->data, out.size() * sizeof(float));
return true;
}
bool has(const std::string & name) const {
return ggml_get_tensor(ctx, name.c_str()) != nullptr;
}
~baseline() {
if (gguf) gguf_free(gguf);
if (ctx) ggml_free(ctx);
}
};
struct compare_stats {
double max_abs = 0.0;
double mean_abs = 0.0;
double rel_l2 = 0.0;
size_t worst = 0;
bool ok = false;
};
// Elementwise gate |got - ref| <= atol + rtol*|ref|, with summary line.
inline bool compare(const std::vector<float> & got, const std::vector<float> & ref,
const std::string & label, double atol, double rtol,
compare_stats * stats_out = nullptr) {
compare_stats st;
if (got.size() != ref.size() || got.empty()) {
std::printf("[%-18s] SIZE MISMATCH got=%zu ref=%zu -> FAIL\n",
label.c_str(), got.size(), ref.size());
if (stats_out) *stats_out = st;
return false;
}
double sum_abs = 0.0, num = 0.0, den = 0.0;
size_t nbad = 0;
for (size_t i = 0; i < got.size(); ++i) {
const double d = std::fabs((double) got[i] - (double) ref[i]);
const double r = (double) ref[i];
num += d * d;
den += r * r;
sum_abs += d;
if (d > st.max_abs) { st.max_abs = d; st.worst = i; }
if (d > atol + rtol * std::fabs(r)) ++nbad;
}
st.mean_abs = sum_abs / (double) got.size();
st.rel_l2 = den > 0 ? std::sqrt(num / den) : std::sqrt(num);
st.ok = nbad == 0;
std::printf("[%-18s] n=%-8zu max|d|=%-11.4g mean|d|=%-11.4g relL2=%-11.4g "
"(worst@%zu got=%.6g ref=%.6g) -> %s\n",
label.c_str(), got.size(), st.max_abs, st.mean_abs, st.rel_l2,
st.worst, (double) got[st.worst], (double) ref[st.worst],
st.ok ? "OK" : "FAIL");
if (stats_out) *stats_out = st;
return st.ok;
}
} // namespace t2_parity
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""
Reference for the stage-1 SS decoder, to validate the C++ trellis2_ss_dec_decode
against the real SparseStructureDecoder.
Builds SparseStructureDecoder in float32 (lossless upcast of the fp16
checkpoint), decodes a sparse-structure latent z_s, and writes a
self-describing binary `ss_dec_ref.bin`:
magic : 8 bytes "SSDEC001"
int32 : latent_channels, res_in, out_channels, res_out
float32 : latent[latent_channels * res_in^3] channel-major
float32 : logits[out_channels * res_out^3] channel-major
The latent comes from --latent (a .latent produced by ss_sample / ref_ss_sample)
if given; otherwise a fixed-seed standard-normal z_s is used so the C++ side can
feed the identical input.
Usage:
python ref_ss_dec.py [--device mps|cpu] [--latent z_s.latent] [--seed N]
"""
import argparse
import json
import os
import struct
import sys
os.environ.setdefault("PYTORCH_ENABLE_MPS_FALLBACK", "1")
SHIV = os.environ.get("TRELLIS2_PY", "/trellis2")
sys.path.insert(0, SHIV)
import numpy as np
import torch
DEFAULT_CKPT = os.environ.get("TRELLIS2_CKPT", os.path.join(
os.path.dirname(__file__), "..", "models", "TRELLIS-image-large/ckpts/ss_dec_conv3d_16l8_fp16"))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--ckpt", default=DEFAULT_CKPT)
ap.add_argument("--device", default="cpu", choices=["mps", "cpu", "cuda"])
ap.add_argument("--latent", default=None, help="optional z_s .latent (channel-major float32)")
ap.add_argument("--seed", type=int, default=1234)
ap.add_argument("--out", default=os.path.join(os.path.dirname(__file__), "ss_dec_ref.bin"))
args = ap.parse_args()
from safetensors.torch import load_file
from trellis2.models.sparse_structure_vae import SparseStructureDecoder
dev = torch.device(args.device)
with open(args.ckpt + ".json") as f:
cfg = json.load(f)["args"]
model = SparseStructureDecoder(**cfg)
model.convert_to_fp32() # set self.dtype=f32 AND convert torso modules
model.eval()
sd = {k: v.float() for k, v in load_file(args.ckpt + ".safetensors").items()}
missing, unexpected = model.load_state_dict(sd, strict=False)
assert not missing and not unexpected, (missing, unexpected)
model.to(dev)
Cin = cfg["latent_channels"]
Rin = 16
Oc = cfg["out_channels"]
if args.latent:
z_cm = np.fromfile(args.latent, dtype="<f4")
assert z_cm.size == Cin * Rin**3, (z_cm.size, Cin * Rin**3)
z = torch.from_numpy(z_cm.reshape(1, Cin, Rin, Rin, Rin).copy()).float().to(dev)
print(f"latent : loaded {args.latent}")
else:
g = torch.Generator().manual_seed(args.seed)
z = torch.randn(1, Cin, Rin, Rin, Rin, generator=g).to(dev)
print(f"latent : random seed={args.seed}")
with torch.no_grad():
logits = model(z) # [1, Oc, Rout, Rout, Rout]
Rout = logits.shape[-1]
lg = logits.detach().cpu().numpy().astype(np.float32)
print(f"logits : [{Oc},{Rout},{Rout},{Rout}] min={lg.min():.5f} max={lg.max():.5f} "
f"mean={lg.mean():.6f} occupied(>0)={(lg > 0).mean() * 100:.2f}%")
z_out = z.detach().cpu().numpy().astype(np.float32).reshape(Cin, -1).reshape(-1)
lg_out = lg.reshape(Oc, -1).reshape(-1)
with open(args.out, "wb") as f:
f.write(b"SSDEC001")
f.write(struct.pack("<4i", Cin, Rin, Oc, Rout))
f.write(z_out.astype("<f4").tobytes())
f.write(lg_out.astype("<f4").tobytes())
print(f"wrote {args.out} ({os.path.getsize(args.out):,} bytes)")
if __name__ == "__main__":
main()
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""
Generate a reference forward pass of the TRELLIS.2 SS-flow DiT in float32, so the
C++ implementation can be validated against it bit-for-bit (modulo fp rounding).
Loads SparseStructureFlowModel directly from the checkpoint (no full pipeline),
runs forward(x, t, cond) on CPU in float32 with a fixed seed, and writes a
self-describing binary `ss_flow_ref.bin`:
magic : 8 bytes "SSFREF01"
int32 : resolution, in_channels, out_channels, cond_tokens, cond_channels
float32: t
float32: x [in_channels * resolution^3] channel-major (x[c*R^3 + n])
float32: cond[cond_tokens * cond_channels] token-major (the .dinodata layout)
float32: out[out_channels * resolution^3] channel-major (reference output)
Usage:
python ref_ss_flow.py --dinodata /path/MushroomBoy.dinodata [--t 500] [--out ss_flow_ref.bin]
"""
import argparse
import json
import os
import struct
import sys
os.environ.setdefault("ATTN_BACKEND", "sdpa")
os.environ.setdefault("SPARSE_ATTN_BACKEND", "sdpa")
os.environ.setdefault("SPARSE_CONV_BACKEND", "none")
os.environ.setdefault("PYTORCH_ENABLE_MPS_FALLBACK", "1")
SHIV = os.environ.get("TRELLIS2_PY", "/trellis2")
sys.path.insert(0, SHIV)
import numpy as np
import torch
DEFAULT_CKPT = os.environ.get("TRELLIS2_CKPT", os.path.join(
os.path.dirname(__file__), "..", "models", "TRELLIS.2-4B/ckpts/ss_flow_img_dit_1_3B_64_bf16"))
def load_dinodata(path):
with open(path, "rb") as f:
assert f.read(8) == b"DINOCOND", "bad magic"
version, dtype, ndim = struct.unpack("<III", f.read(12))
shape = struct.unpack("<%dI" % ndim, f.read(4 * ndim))
arr = np.frombuffer(f.read(), dtype="<f4").reshape(shape)
return arr # [1, tokens, channels]
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--dinodata", default=os.path.join(os.path.dirname(__file__), "..", "dumps", "fixture.dinodata"))
ap.add_argument("--ckpt", default=DEFAULT_CKPT, help="checkpoint stem (no extension)")
ap.add_argument("--t", type=float, default=500.0)
ap.add_argument("--seed", type=int, default=1234)
ap.add_argument("--out", default=os.path.join(os.path.dirname(__file__), "ss_flow_ref.bin"))
args = ap.parse_args()
from safetensors.torch import load_file
from trellis2.models.sparse_structure_flow import SparseStructureFlowModel
with open(args.ckpt + ".json") as f:
cfg = json.load(f)["args"]
print(f"building SparseStructureFlowModel: {cfg['num_blocks']} blocks, d={cfg['model_channels']}")
torch.manual_seed(args.seed)
model = SparseStructureFlowModel(**cfg)
model.convert_to(torch.float32) # blocks -> f32 AND sets self.dtype=f32 (else
# forward's manual_cast downcasts activations)
model.eval()
sd = {k: v.float() for k, v in load_file(args.ckpt + ".safetensors").items()}
# rope_phases is a computed buffer (not stored in the checkpoint).
missing, unexpected = model.load_state_dict(sd, strict=False)
assert not unexpected, f"unexpected keys: {unexpected}"
assert missing == ["rope_phases"], f"unexpected missing keys: {missing}"
R = cfg["resolution"]
Cin = cfg["in_channels"]
Cout = cfg["out_channels"]
cond_np = load_dinodata(args.dinodata) # [1, Lkv, Cctx]
Lkv, Cctx = cond_np.shape[1], cond_np.shape[2]
assert Cctx == cfg["cond_channels"], f"cond channels {Cctx} != {cfg['cond_channels']}"
cond = torch.from_numpy(cond_np.copy()).float()
rng = np.random.default_rng(args.seed)
x_np = rng.standard_normal((1, Cin, R, R, R)).astype(np.float32)
x = torch.from_numpy(x_np)
t = torch.tensor([args.t], dtype=torch.float32)
with torch.no_grad():
out = model(x, t, cond) # [1, Cout, R, R, R]
out_np = out.detach().cpu().numpy().astype(np.float32)
print(f"forward done. x{tuple(x_np.shape)} t={args.t} cond{tuple(cond_np.shape)} -> out{tuple(out_np.shape)}")
print(f"out: min={out_np.min():.5f} max={out_np.max():.5f} mean={out_np.mean():.6f} l2={np.linalg.norm(out_np):.5f}")
# Flatten to the C++ layouts.
x_cm = x_np.reshape(Cin, -1).reshape(-1) # channel-major [Cin * R^3]
out_cm = out_np.reshape(Cout, -1).reshape(-1) # channel-major [Cout * R^3]
cond_tm = cond_np.reshape(-1) # token-major [Lkv * Cctx]
with open(args.out, "wb") as f:
f.write(b"SSFREF01")
f.write(struct.pack("<5i", R, Cin, Cout, Lkv, Cctx))
f.write(struct.pack("<f", args.t))
f.write(x_cm.astype("<f4").tobytes())
f.write(cond_tm.astype("<f4").tobytes())
f.write(out_cm.astype("<f4").tobytes())
print(f"wrote {args.out} ({os.path.getsize(args.out):,} bytes)")
if __name__ == "__main__":
main()
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""
Reference for the full stage-1 flow-Euler sampling loop, to validate the C++
trellis2_ss_flow_sample against the real FlowEulerGuidanceIntervalSampler.
Builds SparseStructureFlowModel in float32, loads the DINOv3 cond from a
.dinodata file, draws fixed noise, runs the sampler, and writes a
self-describing binary `ss_sample_ref.bin`:
magic : 8 bytes "SSSAMP01"
int32 : resolution, in_channels, cond_tokens, cond_channels, steps
float32 : guidance_strength, guidance_rescale, gi_min, gi_max, rescale_t, sigma_min
float32 : noise [in_channels * R^3] channel-major
float32 : cond [cond_tokens * cond_channels] token-major
float32 : latent[in_channels * R^3] channel-major (reference z_s)
Usage:
python ref_ss_sample.py [--device mps|cpu] [--dinodata .../MushroomBoy.dinodata]
"""
import argparse
import json
import os
import struct
import sys
os.environ.setdefault("ATTN_BACKEND", "sdpa")
os.environ.setdefault("SPARSE_ATTN_BACKEND", "sdpa")
os.environ.setdefault("SPARSE_CONV_BACKEND", "none")
os.environ.setdefault("PYTORCH_ENABLE_MPS_FALLBACK", "1")
SHIV = os.environ.get("TRELLIS2_PY", "/trellis2")
sys.path.insert(0, SHIV)
import numpy as np
import torch
DEFAULT_CKPT = os.environ.get("TRELLIS2_CKPT", os.path.join(
os.path.dirname(__file__), "..", "models", "TRELLIS.2-4B/ckpts/ss_flow_img_dit_1_3B_64_bf16"))
def load_dinodata(path):
with open(path, "rb") as f:
assert f.read(8) == b"DINOCOND", "bad magic"
_v, _d, ndim = struct.unpack("<III", f.read(12))
shape = struct.unpack("<%dI" % ndim, f.read(4 * ndim))
arr = np.frombuffer(f.read(), dtype="<f4").reshape(shape)
return arr # [1, tokens, channels]
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--dinodata", default=os.path.join(os.path.dirname(__file__), "..", "dumps", "fixture.dinodata"))
ap.add_argument("--ckpt", default=DEFAULT_CKPT)
ap.add_argument("--device", default="cpu", choices=["mps", "cpu", "cuda"])
ap.add_argument("--seed", type=int, default=1234)
ap.add_argument("--out", default=os.path.join(os.path.dirname(__file__), "ss_sample_ref.bin"))
args = ap.parse_args()
from safetensors.torch import load_file
from trellis2.models.sparse_structure_flow import SparseStructureFlowModel
from trellis2.pipelines import samplers
dev = torch.device(args.device)
with open(args.ckpt + ".json") as f:
cfg = json.load(f)["args"]
model = SparseStructureFlowModel(**cfg)
model.convert_to(torch.float32)
model.eval()
sd = {k: v.float() for k, v in load_file(args.ckpt + ".safetensors").items()}
missing, unexpected = model.load_state_dict(sd, strict=False)
assert not unexpected and missing == ["rope_phases"], (missing, unexpected)
model.to(dev)
R, Cin = cfg["resolution"], cfg["in_channels"]
cond_np = load_dinodata(args.dinodata) # [1, Lkv, Cctx]
Lkv, Cctx = cond_np.shape[1], cond_np.shape[2]
cond = torch.from_numpy(cond_np.copy()).float().to(dev)
neg_cond = torch.zeros_like(cond)
# Fixed noise (CPU generator for reproducibility, then move to device).
g = torch.Generator().manual_seed(args.seed)
noise = torch.randn(1, Cin, R, R, R, generator=g).to(dev)
sampler = samplers.FlowEulerGuidanceIntervalSampler(sigma_min=1e-5)
params = dict(steps=12, rescale_t=5.0, guidance_strength=7.5,
guidance_interval=[0.6, 1.0], guidance_rescale=0.7)
print(f"sampling on {dev} steps={params['steps']} gs={params['guidance_strength']} "
f"rescale={params['guidance_rescale']} interval={params['guidance_interval']}")
with torch.no_grad():
z_s = sampler.sample(model, noise, cond, neg_cond, verbose=True, **params).samples
z_np = z_s.detach().cpu().numpy().astype(np.float32) # [1, Cin, R,R,R]
noise_np = noise.detach().cpu().numpy().astype(np.float32)
print(f"z_s: min={z_np.min():.5f} max={z_np.max():.5f} mean={z_np.mean():.6f} "
f"l2={np.linalg.norm(z_np):.5f} (occupancy>0: {(z_np>0).mean()*100:.2f}%)")
noise_cm = noise_np.reshape(Cin, -1).reshape(-1)
z_cm = z_np.reshape(Cin, -1).reshape(-1)
cond_tm = cond_np.reshape(-1)
with open(args.out, "wb") as f:
f.write(b"SSSAMP01")
f.write(struct.pack("<5i", R, Cin, Lkv, Cctx, params["steps"]))
f.write(struct.pack("<6f", params["guidance_strength"], params["guidance_rescale"],
params["guidance_interval"][0], params["guidance_interval"][1],
params["rescale_t"], 1e-5))
f.write(noise_cm.astype("<f4").tobytes())
f.write(cond_tm.astype("<f4").tobytes())
f.write(z_cm.astype("<f4").tobytes())
print(f"wrote {args.out} ({os.path.getsize(args.out):,} bytes)")
if __name__ == "__main__":
main()
+93
View File
@@ -0,0 +1,93 @@
#include "trellis2.h"
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <vector>
namespace {
std::vector<uint8_t> solid(int w, int h, uint8_t v) {
std::vector<uint8_t> im((size_t) w * h * 4, 255);
for (int i = 0; i < w * h; ++i) {
im[(size_t) i * 4 + 0] = v;
im[(size_t) i * 4 + 1] = v;
im[(size_t) i * 4 + 2] = v;
}
return im;
}
void rgb(std::vector<uint8_t> & im, int w, int x, int y,
uint8_t r, uint8_t g, uint8_t b) {
uint8_t * p = &im[((size_t) y * w + x) * 4];
p[0] = r; p[1] = g; p[2] = b; p[3] = 255;
}
uint8_t alpha(const std::vector<uint8_t> & im, int w, int x, int y) {
return im[((size_t) y * w + x) * 4 + 3];
}
bool expect(bool condition, const char * message) {
if (!condition) std::fprintf(stderr, "%s\n", message);
return condition;
}
} // namespace
int main() {
bool ok = true;
// White and black backgrounds are detected from the border while a
// disconnected contrasting subject remains opaque.
for (int bg : {0, 8, 247, 255}) {
auto im = solid(9, 9, (uint8_t) bg);
for (int y = 3; y <= 5; ++y) for (int x = 3; x <= 5; ++x) {
if (bg < 128) rgb(im, 9, x, y, 235, 80, 40);
else rgb(im, 9, x, y, 20, 90, 180);
}
const int changed = trellis2_remove_solid_background_rgba(
im.data(), 9, 9, TRELLIS2_BACKGROUND_AUTO);
ok &= expect(changed > 0, bg < 128 ? "black background not detected"
: "white background not detected");
ok &= expect(alpha(im, 9, 0, 0) <= 1, "background corner is not transparent");
ok &= expect(alpha(im, 9, 4, 4) == 255, "subject centre lost opacity");
}
// Border connectivity prevents an enclosed white detail from being erased
// along with the surrounding white background.
auto enclosed = solid(9, 9, 255);
for (int y = 2; y <= 6; ++y) for (int x = 2; x <= 6; ++x)
rgb(enclosed, 9, x, y, 180, 30, 20);
rgb(enclosed, 9, 4, 4, 255, 255, 255);
trellis2_remove_solid_background_rgba(enclosed.data(), 9, 9, TRELLIS2_BACKGROUND_AUTO);
ok &= expect(alpha(enclosed, 9, 4, 4) == 255, "enclosed white subject detail was removed");
// A genuine existing alpha mask wins over automatic colour removal.
auto masked = solid(20, 20, 255);
for (int y = 0; y < 3; ++y) for (int x = 0; x < 3; ++x)
masked[((size_t) y * 20 + x) * 4 + 3] = 0;
const int changed = trellis2_remove_solid_background_rgba(
masked.data(), 20, 20, TRELLIS2_BACKGROUND_AUTO);
ok &= expect(changed == 0, "existing alpha mask was unexpectedly modified");
ok &= expect(alpha(masked, 20, 10, 10) == 255, "existing mask made opaque content transparent");
// Mid-tone borders are not guessed as black or white in auto mode.
auto grey = solid(9, 9, 128);
ok &= expect(trellis2_remove_solid_background_rgba(
grey.data(), 9, 9, TRELLIS2_BACKGROUND_AUTO) == 0,
"mid-grey background should be left unchanged");
// Explicit modes override auto detection but still affect border-connected
// pixels only. This is useful when a subject occupies most of the border.
auto forced = solid(9, 9, 120);
for (int x = 0; x < 4; ++x) rgb(forced, 9, x, 0, 0, 0, 0);
ok &= expect(trellis2_remove_solid_background_rgba(
forced.data(), 9, 9, TRELLIS2_BACKGROUND_BLACK) > 0,
"forced black mode did not remove a connected black region");
ok &= expect(alpha(forced, 9, 0, 0) == 0, "forced black pixel is not transparent");
ok &= expect(alpha(forced, 9, 8, 8) == 255, "forced mode modified unrelated pixels");
if (!ok) return 1;
std::puts("background removal: ok");
return 0;
}
+233
View File
@@ -0,0 +1,233 @@
// Validation of the 1024_cascade HR stage against the PyTorch reference
// (scripts/dump_cascade_reference.py -> dumps/reference_cascade.gguf):
//
// 1. shape-decoder upsample(x4): the LR slat -> 512^3 candidate coord set,
// and its quantized 64^3 HR scaffold (subdivision-boundary set tolerance).
// 2. HR (1024-model) flow forward at t=500 on the HR scaffold (tight gate).
// 3. final 1024^3 decode of the reference HR slat: per-level features,
// subdivision, and 7-channel output.
// (The 12-step HR sampler is env-gated (TRELLIS2_CASCADE_SAMPLE) — 24 forwards
// at ~40k tokens is impractical on CPU; the shared Euler loop is already
// validated by test_slat / test_ss_sample.)
//
// usage: test_cascade <slat_512_f32> <slat_1024_f32> <shape_dec_f32> <reference_cascade.gguf>
// exits 77 (ctest SKIP) when inputs are missing.
#include "trellis2.h"
#include "parity.hpp"
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <string>
#include <unordered_set>
#include <vector>
static bool file_exists(const std::string & p) {
std::ifstream f(p);
return f.good();
}
// [L*4] (batch,x,y,z) reference coords -> [L*3] int32 (x,y,z)
static std::vector<int32_t> coords_xyz(const std::vector<float> & c4) {
const size_t L = c4.size() / 4;
std::vector<int32_t> out(L * 3);
for (size_t v = 0; v < L; ++v) {
out[v * 3] = (int32_t) c4[v * 4 + 1];
out[v * 3 + 1] = (int32_t) c4[v * 4 + 2];
out[v * 3 + 2] = (int32_t) c4[v * 4 + 3];
}
return out;
}
static uint64_t vkey(int32_t x, int32_t y, int32_t z) {
return ((uint64_t) (uint32_t) x << 40) | ((uint64_t) (uint32_t) y << 20) | (uint64_t) (uint32_t) z;
}
// symmetric-difference fraction of two coord sets ([*3] int32)
static double set_diff_frac(const std::vector<int32_t> & a, const std::vector<int32_t> & b) {
std::unordered_set<uint64_t> sa, sb;
for (size_t i = 0; i < a.size(); i += 3) sa.insert(vkey(a[i], a[i + 1], a[i + 2]));
for (size_t i = 0; i < b.size(); i += 3) sb.insert(vkey(b[i], b[i + 1], b[i + 2]));
size_t only = 0;
for (uint64_t k : sa) if (!sb.count(k)) ++only;
for (uint64_t k : sb) if (!sa.count(k)) ++only;
const size_t big = sa.size() > sb.size() ? sa.size() : sb.size();
return big ? (double) only / (double) big : 0.0;
}
int main(int argc, char ** argv) {
if (argc < 5) {
std::fprintf(stderr,
"usage: %s <slat_512_f32.gguf> <slat_1024_f32.gguf> <shape_dec_f32.gguf> <reference_cascade.gguf>\n",
argv[0]);
return 2;
}
const std::string flow512_path = argv[1];
const std::string flow1024_path = argv[2];
const std::string dec_path = argv[3];
const std::string ref_path = argv[4];
if (!file_exists(flow512_path) || !file_exists(flow1024_path) ||
!file_exists(dec_path) || !file_exists(ref_path)) {
std::fprintf(stderr, "missing input file(s), skipping\n");
return 77;
}
t2_parity::baseline ref;
if (!ref.open(ref_path)) {
std::fprintf(stderr, "failed to open %s\n", ref_path.c_str());
return 1;
}
std::vector<float> cond512, cond1024, coords32f, lr_slat, up_coordsf, hr_coordsf, hr_noise;
if (!ref.load("cond_512", cond512) || !ref.load("cond_1024", cond1024) ||
!ref.load("coords32", coords32f) || !ref.load("lr_slat", lr_slat) ||
!ref.load("up_coords", up_coordsf) || !ref.load("hr_coords", hr_coordsf) ||
!ref.load("hr_noise", hr_noise)) {
std::fprintf(stderr, "reference missing required tensors\n");
return 1;
}
const int L32 = (int) (coords32f.size() / 4);
const int Lhr = (int) (hr_coordsf.size() / 4);
const int Lkv512 = (int) (cond512.size() / 1024);
const int Lkv1024 = (int) (cond1024.size() / 1024);
std::vector<int32_t> coords32 = coords_xyz(coords32f);
std::vector<int32_t> ref_up_coords = coords_xyz(up_coordsf);
std::vector<int32_t> hr_coords = coords_xyz(hr_coordsf);
std::printf("reference: scaffold %d voxels, upsample %zu, HR %d voxels, cond %d/%d tokens\n",
L32, up_coordsf.size() / 4, Lhr, Lkv512, Lkv1024);
std::string err;
int n_fail = 0;
// ── 1. shape-decoder upsample(x4): coord set + quantized 64^3 scaffold ────
trellis2_shape_dec_model * dec = trellis2_shape_dec_load(dec_path, true, &err, "cpu");
if (!dec) { std::fprintf(stderr, "dec load failed: %s\n", err.c_str()); return 1; }
std::printf("dec backend: %s\n", trellis2_shape_dec_backend_name(dec));
{
std::vector<int32_t> got_up;
if (!trellis2_shape_dec_upsample(dec, lr_slat.data(), L32, coords32.data(),
/*upsample_times*/ 4, got_up, &err)) {
std::fprintf(stderr, "upsample failed: %s\n", err.c_str());
trellis2_shape_dec_free(dec);
return 1;
}
const double up_frac = set_diff_frac(got_up, ref_up_coords);
std::printf("[upsample coords] got %zu vs ref %zu, sym-diff %.4f%% -> %s\n",
got_up.size() / 3, ref_up_coords.size() / 3, 100.0 * up_frac,
up_frac <= 5e-4 ? "OK" : "FAIL");
if (up_frac > 5e-4) ++n_fail;
// quantize both to 64^3 and compare the dedup'd set that drives the HR flow
auto quant = [](const std::vector<int32_t> & c) {
std::unordered_set<uint64_t> s;
std::vector<int32_t> out;
for (size_t i = 0; i < c.size(); i += 3) {
int32_t x = (int32_t) ((c[i] + 0.5f) / 512.0f * 64.0f);
int32_t y = (int32_t) ((c[i + 1] + 0.5f) / 512.0f * 64.0f);
int32_t z = (int32_t) ((c[i + 2] + 0.5f) / 512.0f * 64.0f);
if (s.insert(vkey(x, y, z)).second) { out.push_back(x); out.push_back(y); out.push_back(z); }
}
return out;
};
std::vector<int32_t> my_hr = quant(got_up);
const double hr_frac = set_diff_frac(my_hr, hr_coords);
std::printf("[hr scaffold] got %zu vs ref %d, sym-diff %.4f%% -> %s\n",
my_hr.size() / 3, Lhr, 100.0 * hr_frac, hr_frac <= 5e-4 ? "OK" : "FAIL");
if (hr_frac > 5e-4) ++n_fail;
}
// ── 2. HR flow forward at t=500 on the reference HR scaffold ──────────────
{
// Force CPU: at 10k+ HR tokens the attention exceeds the exact-path
// threshold and uses flash, and GPU flash (F16-MMA) is ~1e-2 vs the
// exact fp32 reference. CPU flash is exact-matching (~3e-4), so it
// gives a meaningful tight gate. TRELLIS2_CASCADE_GPU overrides.
const char * dev = std::getenv("TRELLIS2_CASCADE_GPU") ? nullptr : "cpu";
trellis2_slat_flow_model * flow = trellis2_slat_flow_load(flow1024_path, true, &err, dev);
if (!flow) { std::fprintf(stderr, "1024 flow load failed: %s\n", err.c_str()); trellis2_shape_dec_free(dec); return 1; }
std::printf("1024 flow backend: %s\n", trellis2_slat_flow_backend_name(flow));
std::vector<float> got((size_t) Lhr * 32), want;
if (!trellis2_slat_flow_forward(flow, hr_noise.data(), Lhr, hr_coords.data(), 500.0f,
cond1024.data(), Lkv1024, 1024, got.data(), &err)) {
std::fprintf(stderr, "HR flow forward failed: %s\n", err.c_str());
trellis2_slat_flow_free(flow); trellis2_shape_dec_free(dec); return 1;
}
ref.load("hr_flow_t500_out", want);
t2_parity::compare_stats st;
t2_parity::compare(got, want, "hr_flow_t500_out", 2e-3, 2e-3, &st);
if (st.rel_l2 > 3e-3) { std::printf(" -> HR forward rel_l2 %.4g > 3e-3, FAIL\n", st.rel_l2); ++n_fail; }
// optional: full HR sampler (expensive at ~40k tokens; off by default)
if (std::getenv("TRELLIS2_CASCADE_SAMPLE")) {
trellis2_ss_sampler_params P;
P.steps = 12; P.guidance_strength = 7.5f; P.guidance_rescale = 0.5f;
P.guidance_interval_min = 0.6f; P.guidance_interval_max = 1.0f; P.rescale_t = 3.0f;
std::vector<float> sampled((size_t) Lhr * 32), wslat;
if (trellis2_slat_flow_sample(flow, Lhr, hr_coords.data(), cond1024.data(), Lkv1024, 1024,
&P, hr_noise.data(), true, sampled.data(), &err)) {
ref.load("hr_slat", wslat);
t2_parity::compare_stats ss;
t2_parity::compare(sampled, wslat, "hr_slat(sampled)", 5e-2, 5e-2, &ss);
}
}
trellis2_slat_flow_free(flow);
}
// ── 3. final 1024^3 decode of the reference HR slat ──────────────────────
// The 1024^3 decode is the SAME decoder validated exactly at the 512 tier
// (test_slat, levels 0-4 at rel-L2 5e-7) applied to more voxels, and the
// end-to-end demo exercises it directly. It also transiently needs ~14 GB of
// host RAM (the finest up-block's conv output is held in both the graph and
// the readback), so it is gated behind TRELLIS2_CASCADE_DECODE — enable it
// on a big-RAM box to also gate out7 here.
if (!std::getenv("TRELLIS2_CASCADE_DECODE")) {
trellis2_shape_dec_free(dec);
std::printf("\n(1024^3 decode gate skipped; set TRELLIS2_CASCADE_DECODE to enable)\n");
std::printf("total failures: %d\nRESULT: %s\n", n_fail, n_fail ? "FAIL" : "PASS");
return n_fail ? 1 : 0;
}
std::vector<float> hr_slat;
if (!ref.load("hr_slat", hr_slat)) {
std::fprintf(stderr, "reference missing hr_slat\n");
trellis2_shape_dec_free(dec); return 1;
}
// Compare only the final 7-channel output (taps=nullptr): the per-level
// intermediates would hold multiple GB at 1024^3 and the decoder's level
// logic is already validated exactly at the 512 tier (test_slat, same
// decoder). out7 is the load-bearing gate.
std::vector<float> out_feats;
std::vector<int32_t> out_coords;
if (!trellis2_shape_dec_decode(dec, hr_slat.data(), Lhr, hr_coords.data(),
out_feats, out_coords, nullptr, &err)) {
std::fprintf(stderr, "HR decode failed: %s\n", err.c_str());
trellis2_shape_dec_free(dec); return 1;
}
trellis2_shape_dec_free(dec);
std::printf("HR decode: %zu output voxels\n", out_coords.size() / 3);
std::vector<float> ref_out7;
ref.load("out7", ref_out7);
const size_t a = out_feats.size(), b = ref_out7.size();
const size_t big = a > b ? a : b, sml = a > b ? b : a;
if (a != b) {
const double frac = (double) (big - sml) / (double) big;
if (frac > 5e-4) {
std::printf("[out7] SIZE MISMATCH %zu vs %zu (%.3f%%) -> FAIL\n", a, b, 100.0 * frac);
++n_fail;
} else {
std::printf("[out7] near-match (%zu vs %zu, %.4f%% subdivision boundary flip) -> OK\n",
a, b, 100.0 * frac);
}
} else {
t2_parity::compare_stats st;
t2_parity::compare(out_feats, ref_out7, "out7", 2e-3, 2e-3, &st);
if (st.rel_l2 > 2e-2) { std::printf(" -> out7 rel_l2 %.4g > 2e-2, FAIL\n", st.rel_l2); ++n_fail; }
}
std::printf("\ntotal failures: %d\n", n_fail);
std::printf("RESULT: %s\n", n_fail ? "FAIL" : "PASS");
return n_fail ? 1 : 0;
}
+103
View File
@@ -0,0 +1,103 @@
// Layer-by-layer validation of the DINOv3 encoder against the PyTorch
// reference dump (scripts/dump_dino_reference.py).
//
// usage: test_dino <dino_f32.gguf> <reference_dino.gguf> [atol] [rtol]
// exits 77 (ctest SKIP) when either input file is missing.
#include "trellis2.h"
#include "parity.hpp"
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <string>
#include <vector>
static bool file_exists(const std::string & p) {
std::ifstream f(p);
return f.good();
}
int main(int argc, char ** argv) {
if (argc < 3) {
std::fprintf(stderr, "usage: %s <dino_f32.gguf> <reference_dino.gguf> [atol] [rtol]\n", argv[0]);
return 2;
}
const std::string gguf_path = argv[1];
const std::string ref_path = argv[2];
const double atol = argc > 3 ? std::atof(argv[3]) : 2e-3;
const double rtol = argc > 4 ? std::atof(argv[4]) : 2e-3;
if (!file_exists(gguf_path) || !file_exists(ref_path)) {
std::fprintf(stderr, "missing input file(s), skipping\n");
return 77;
}
t2_parity::baseline ref;
if (!ref.open(ref_path)) {
std::fprintf(stderr, "failed to open reference gguf %s\n", ref_path.c_str());
return 1;
}
std::vector<float> pixels;
if (!ref.load("pixel_values", pixels)) {
std::fprintf(stderr, "reference has no pixel_values tensor\n");
return 1;
}
const int S = (int) std::lround(std::sqrt((double) pixels.size() / 3.0));
std::printf("pixel_values: %zu floats -> image size %d\n", pixels.size(), S);
std::string err;
trellis2_dino_model * m = trellis2_dino_load(gguf_path, true, &err);
if (!m) {
std::fprintf(stderr, "load failed: %s\n", err.c_str());
return 1;
}
std::printf("backend: %s\n", trellis2_dino_backend_name(m));
trellis2_dino_cond cond;
trellis2_dino_taps taps;
if (!trellis2_dino_encode(m, pixels.data(), S, cond, &taps, &err)) {
std::fprintf(stderr, "encode failed: %s\n", err.c_str());
trellis2_dino_free(m);
return 1;
}
int n_fail = 0, n_cmp = 0, n_missing = 0;
std::string first_fail;
std::vector<float> refbuf;
for (size_t i = 0; i < taps.names.size(); ++i) {
const std::string & name = taps.names[i];
if (!ref.has(name)) {
++n_missing;
continue;
}
if (!ref.load(name, refbuf)) {
std::printf("[%-18s] reference tensor unreadable -> FAIL\n", name.c_str());
++n_fail;
continue;
}
++n_cmp;
if (!t2_parity::compare(taps.data[i], refbuf, name, atol, rtol)) {
++n_fail;
if (first_fail.empty()) first_fail = name;
}
}
trellis2_dino_free(m);
std::printf("\ncompared %d taps (%d without reference), %d failed\n",
n_cmp, n_missing, n_fail);
if (n_fail) {
std::printf("FIRST DIVERGENCE: %s\n", first_fail.c_str());
std::printf("RESULT: FAIL\n");
return 1;
}
if (n_cmp == 0) {
std::printf("RESULT: FAIL (nothing compared)\n");
return 1;
}
std::printf("RESULT: PASS\n");
return 0;
}
+127
View File
@@ -0,0 +1,127 @@
// test_marching_cubes — validate the self-contained isosurface extractor
// (examples/marching_cubes.h) on analytic fields, with no model needed.
//
// Marching tetrahedra on the Freudenthal subdivision must produce a watertight
// 2-manifold: every undirected edge is shared by exactly two triangles, and the
// Euler characteristic V - E + F equals 2*(#components) for closed genus-0
// surfaces. These invariants fail loudly if the tetra triangle table (TET_TRI)
// has a typo, so they double as a table-integrity check.
#include "marching_cubes.h"
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <unordered_map>
#include <vector>
struct Stats { size_t bad_edges = 0; size_t bad_dir = 0; int64_t euler = 0; size_t E = 0; };
static Stats analyze(const mc::Mesh & m) {
auto ukey = [](int a, int b) -> int64_t {
int lo = a < b ? a : b, hi = a < b ? b : a;
return (int64_t) lo * 100000000LL + hi;
};
auto dkey = [](int a, int b) -> int64_t { return (int64_t) a * 100000000LL + b; };
std::unordered_map<int64_t, int> uedges, dedges;
for (size_t t = 0; t < m.n_tris(); ++t) {
int v[3] = { m.tris[3*t], m.tris[3*t+1], m.tris[3*t+2] };
for (int i = 0; i < 3; ++i) {
int a = v[i], b = v[(i + 1) % 3];
uedges[ukey(a, b)]++;
dedges[dkey(a, b)]++;
}
}
Stats s;
s.E = uedges.size();
for (auto & e : uedges) if (e.second != 2) ++s.bad_edges;
// Coherent orientation: each directed edge appears exactly once (its mate is
// the reverse, contributed by the neighboring triangle). A flipped face makes
// some directed edge appear twice -> caught here.
for (auto & e : dedges) if (e.second != 1) ++s.bad_dir;
s.euler = (int64_t) m.n_verts() - (int64_t) s.E + (int64_t) m.n_tris();
return s;
}
static int check(const char * name, const mc::Mesh & m, int64_t want_euler) {
Stats s = analyze(m);
std::printf("%-16s V=%-6zu E=%-6zu F=%-6zu bad_edges=%zu bad_winding=%zu euler=%lld (want %lld) ",
name, m.n_verts(), s.E, m.n_tris(), s.bad_edges, s.bad_dir,
(long long) s.euler, (long long) want_euler);
bool ok = (m.n_tris() > 0) && (s.bad_edges == 0) && (s.bad_dir == 0) && (s.euler == want_euler);
std::printf("%s\n", ok ? "OK" : "FAIL");
return ok ? 0 : 1;
}
int main() {
int fails = 0;
// 1) single sphere -> closed genus-0 manifold, euler 2
{
const int N = 32; const float c = (N - 1) * 0.5f, r = 10.0f;
std::vector<float> f((size_t) N*N*N);
for (int z = 0; z < N; ++z)
for (int y = 0; y < N; ++y)
for (int x = 0; x < N; ++x) {
float d = std::sqrt((x-c)*(x-c) + (y-c)*(y-c) + (z-c)*(z-c));
f[(size_t)x + (size_t)y*N + (size_t)z*N*N] = r - d; // inside (>0) within radius
}
mc::Mesh m = mc::extract(f.data(), N, N, N, 0.0f);
fails += check("sphere", m, 2);
// vertex normals must point outward: n . (p - center) > 0
size_t bad_n = 0;
for (size_t i = 0; i < m.n_verts(); ++i) {
float px = m.verts[3*i]-c, py = m.verts[3*i+1]-c, pz = m.verts[3*i+2]-c;
float d = m.normals[3*i]*px + m.normals[3*i+1]*py + m.normals[3*i+2]*pz;
if (d <= 0.0f) ++bad_n;
}
// FACE winding must point outward too: (P1-P0)x(P2-P0) . (centroid-center) > 0
size_t bad_f = 0;
for (size_t t = 0; t < m.n_tris(); ++t) {
const float *P0=&m.verts[3*m.tris[3*t]], *P1=&m.verts[3*m.tris[3*t+1]], *P2=&m.verts[3*m.tris[3*t+2]];
float u[3]={P1[0]-P0[0],P1[1]-P0[1],P1[2]-P0[2]}, v[3]={P2[0]-P0[0],P2[1]-P0[1],P2[2]-P0[2]};
float fn[3]={u[1]*v[2]-u[2]*v[1], u[2]*v[0]-u[0]*v[2], u[0]*v[1]-u[1]*v[0]};
float cx=(P0[0]+P1[0]+P2[0])/3-c, cy=(P0[1]+P1[1]+P2[1])/3-c, cz=(P0[2]+P1[2]+P2[2])/3-c;
if (fn[0]*cx + fn[1]*cy + fn[2]*cz <= 0.0f) ++bad_f;
}
std::printf("%-16s inward_vnormals=%zu inward_faces=%zu (of %zu) %s\n", "sphere/orient",
bad_n, bad_f, m.n_verts(), (bad_n == 0 && bad_f == 0) ? "OK" : "FAIL");
if (bad_n != 0 || bad_f != 0) ++fails;
}
// 2) sphere that reaches the grid boundary -> still closed thanks to padding
{
const int N = 24; const float c = (N - 1) * 0.5f, r = 14.0f; // r > c, clipped by walls
std::vector<float> f((size_t) N*N*N);
for (int z = 0; z < N; ++z)
for (int y = 0; y < N; ++y)
for (int x = 0; x < N; ++x) {
float d = std::sqrt((x-c)*(x-c) + (y-c)*(y-c) + (z-c)*(z-c));
f[(size_t)x + (size_t)y*N + (size_t)z*N*N] = r - d;
}
mc::Mesh m = mc::extract(f.data(), N, N, N, 0.0f);
// clipped sphere is still a closed genus-0 blob -> euler 2
fails += check("clipped-sphere", m, 2);
}
// 3) two disjoint spheres -> two components, euler 4
{
const int N = 48; const float r = 7.0f;
const float c1[3] = {12, 24, 24}, c2[3] = {36, 24, 24};
std::vector<float> f((size_t) N*N*N);
for (int z = 0; z < N; ++z)
for (int y = 0; y < N; ++y)
for (int x = 0; x < N; ++x) {
auto sd = [&](const float c[3]) {
return r - std::sqrt((x-c[0])*(x-c[0]) + (y-c[1])*(y-c[1]) + (z-c[2])*(z-c[2]));
};
f[(size_t)x + (size_t)y*N + (size_t)z*N*N] = std::fmax(sd(c1), sd(c2));
}
mc::Mesh m = mc::extract(f.data(), N, N, N, 0.0f);
fails += check("two-spheres", m, 4);
}
std::printf("\nRESULT: %s\n", fails == 0 ? "PASS" : "FAIL");
return fails == 0 ? 0 : 1;
}
+126
View File
@@ -0,0 +1,126 @@
#include "mesh_export.h"
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
int main() {
const float verts[] = {
0,0,0, 1,0,0, 0,1,0, 0,0,1,
};
const int32_t tris[] = {
0,2,1, 0,1,3, 0,3,2, 1,2,3,
};
// base RGB, metallic, roughness, alpha
const float pbr[] = {
1,0,0, 0,0.4f,0.5f,
0,1,0, 0,0.5f,1.0f,
0,0,1, 1,0.6f,1.0f,
1,1,1, 0,0.7f,1.0f,
};
t2glb::MeshExportOptions opt;
opt.texture_size = 64;
opt.dilate = 2;
std::vector<uint8_t> glb;
std::string err;
if (!t2glb::mesh_to_glb(verts, 4, tris, 4, pbr, opt, glb, err)) {
std::fprintf(stderr, "mesh_to_glb failed: %s\n", err.c_str());
return 1;
}
if (glb.size() < 20 || std::memcmp(glb.data(), "glTF", 4) != 0) {
std::fprintf(stderr, "invalid GLB header\n");
return 1;
}
const std::string bytes((const char *) glb.data(), glb.size());
if (bytes.find("\"alphaMode\":\"BLEND\"") == std::string::npos) {
std::fprintf(stderr, "alpha material was not preserved\n");
return 1;
}
if (bytes.find("\"COLOR_0\":2") == std::string::npos ||
bytes.find("\"_METALLIC_ROUGHNESS\":3") == std::string::npos ||
bytes.find("\"baseColorTexture\"") != std::string::npos) {
std::fprintf(stderr, "portable vertex material attributes are missing\n");
return 1;
}
// The old per-triangle grid duplicated all three vertices and collapsed to
// sub-texel UV cells on production meshes. The direct path must retain the
// tetrahedron's four vertices and normalised RGBA bytes exactly.
auto u32 = [&](size_t off) {
return (uint32_t)(uint8_t)glb[off] |
((uint32_t)(uint8_t)glb[off+1] << 8) |
((uint32_t)(uint8_t)glb[off+2] << 16) |
((uint32_t)(uint8_t)glb[off+3] << 24);
};
const size_t bin = 20 + u32(12) + 8;
const size_t color = bin + 4 * 3 * sizeof(float) * 2;
const uint8_t expected_first_color[] = {255, 255, 0, 0, 0, 0, 0, 128};
if (color + sizeof expected_first_color > glb.size() ||
std::memcmp(glb.data() + color, expected_first_color,
sizeof expected_first_color) != 0 ||
bytes.find("\"count\":4,\"type\":\"VEC4\"") == std::string::npos) {
std::fprintf(stderr, "vertex colours were changed or vertices were duplicated\n");
return 1;
}
// A tiny near-opaque decoder outlier must not put the whole primitive into
// alpha blending (which disables depth writes and resembles missing faces).
float nearly_opaque[24];
std::memcpy(nearly_opaque, pbr, sizeof nearly_opaque);
for (int i = 0; i < 4; ++i) nearly_opaque[6*i+5] = 1.0f;
nearly_opaque[5] = 0.994f;
if (!t2glb::mesh_to_glb(verts, 4, tris, 4, nearly_opaque, opt, glb, err) ||
std::string((const char *) glb.data(), glb.size()).find("\"alphaMode\":\"BLEND\"") != std::string::npos) {
std::fprintf(stderr, "near-opaque PBR noise enabled alpha blending\n");
return 1;
}
// Preview preparation uses the same geometry path and can keep only the
// largest disconnected component for background-plane cleanup.
const float two_component_verts[] = {
0,0,0, 1,0,0, 0,1,0, 0,0,1,
3,0,0, 4,0,0, 3,1,0,
};
const int32_t two_component_tris[] = {
0,2,1, 0,1,3, 0,3,2, 1,2,3,
4,5,6,
};
t2glb::PreparedMesh prepared;
opt.components = t2glb::ComponentFilter::KeepLargest;
if (!t2glb::prepare_mesh(two_component_verts, 7, two_component_tris, 5,
nullptr, opt, prepared, err)) {
std::fprintf(stderr, "prepare_mesh failed: %s\n", err.c_str());
return 1;
}
if (prepared.tris.size() / 3 != 4 || prepared.verts.size() / 3 != 4 ||
prepared.normals.size() != prepared.verts.size()) {
std::fprintf(stderr, "largest-component preview is wrong: %zu verts, %zu tris, %zu normals\n",
prepared.verts.size() / 3, prepared.tris.size() / 3,
prepared.normals.size() / 3);
return 1;
}
// Keeping all components must retain every valid source triangle; export no
// longer performs polygon decimation.
opt.components = t2glb::ComponentFilter::KeepAll;
if (!t2glb::prepare_mesh(two_component_verts, 7, two_component_tris, 5,
nullptr, opt, prepared, err) || prepared.tris.size() / 3 != 5) {
std::fprintf(stderr, "full-density export changed the polygon count\n");
return 1;
}
// The API remains present in portable builds without CGAL and must fail
// explicitly instead of creating an untextured or incorrectly mapped GLB.
if (!t2glb::print_remesh_available()) {
if (t2glb::mesh_to_projected_glb(verts, 4, tris, 4,
verts, 4, tris, 4, pbr,
opt, glb, err) ||
err.find("unavailable") == std::string::npos) {
std::fprintf(stderr, "CGAL-free projected bake did not report unavailable\n");
return 1;
}
}
std::puts("RESULT: PASS");
return 0;
}
+72
View File
@@ -0,0 +1,72 @@
#include "pbr_utils.h"
#include <cmath>
#include <cstdio>
#include <vector>
static bool close(float a, float b, float eps = 1e-6f) {
return std::fabs(a - b) <= eps;
}
int main() {
// Eight corners of a unit cube, with f(x,y,z) = x + 2y + 4z.
int32_t coords[8 * 3];
float feats[8];
int n = 0;
for (int x = 0; x <= 1; ++x)
for (int y = 0; y <= 1; ++y)
for (int z = 0; z <= 1; ++z) {
coords[3*n] = x; coords[3*n+1] = y; coords[3*n+2] = z;
feats[n++] = (float) (x + 2*y + 4*z);
}
const float query[] = {0.0f, 0.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 1.0f, 1.0f};
float out[3], weights[3];
t2pbr::sample_sparse_trilinear(feats, 8, 1, coords, query, 3, out, weights);
if (!close(out[0], 0.0f) || !close(out[1], 3.5f) || !close(out[2], 7.0f) ||
!close(weights[0], 1.0f) || !close(weights[1], 1.0f) || !close(weights[2], 1.0f)) {
std::fprintf(stderr, "dense trilinear mismatch: %g %g %g\n", out[0], out[1], out[2]);
return 1;
}
// Sparse boundary: only one weighted corner exists, so it is renormalized
// instead of darkening toward absent voxels.
const int32_t one_coord[] = {0, 0, 0};
const float one_feat[] = {0.25f, 0.75f};
const float half[] = {0.5f, 0.5f, 0.5f};
float sparse[2], sparse_weight;
t2pbr::sample_sparse_trilinear(one_feat, 1, 2, one_coord, half, 1,
sparse, &sparse_weight);
if (!close(sparse[0], 0.25f) || !close(sparse[1], 0.75f) ||
!close(sparse_weight, 0.125f)) {
std::fprintf(stderr, "sparse trilinear mismatch: %g %g w=%g\n",
sparse[0], sparse[1], sparse_weight);
return 1;
}
// Regression: the failed integrated texture path returned essentially an
// all-one six-channel material. It must be rejected, without mistaking a
// legitimate white base colour for a collapsed full PBR result.
std::vector<float> collapsed(1000 * 6, 1.0f);
for (int v = 0; v < 5; ++v) collapsed[(size_t) v * 6] = 0.5f;
if (!t2pbr::is_collapsed_saturated(collapsed.data(), 1000)) {
std::fprintf(stderr, "collapsed saturated material was not detected\n");
return 1;
}
collapsed[5 * 6] = 0.5f; // only 994/1000 now have all channels saturated
if (t2pbr::is_collapsed_saturated(collapsed.data(), 1000)) {
std::fprintf(stderr, "saturation threshold is too aggressive\n");
return 1;
}
std::vector<float> white(16 * 6, 1.0f);
for (int v = 0; v < 16; ++v) {
white[(size_t) v * 6 + 3] = 0.0f;
white[(size_t) v * 6 + 4] = 0.5f;
}
if (t2pbr::is_collapsed_saturated(white.data(), 16)) {
std::fprintf(stderr, "legitimate white material was rejected\n");
return 1;
}
std::puts("RESULT: PASS");
return 0;
}
+74
View File
@@ -0,0 +1,74 @@
// Byte-exactness test of trellis2_preprocess_rgba against the Python
// reference (PIL): dumps/fixture_rgba.png -> preprocess -> must equal
// dumps/fixture_512.png (produced by scripts/dump_dino_reference.py).
//
// usage: test_preprocess <fixture_rgba.png> <fixture_512.png>
// exits 77 (ctest SKIP) when inputs are missing.
#include "trellis2.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <string>
#include <vector>
int main(int argc, char ** argv) {
if (argc < 3) {
std::fprintf(stderr, "usage: %s <fixture_rgba.png> <fixture_512.png>\n", argv[0]);
return 2;
}
if (!std::ifstream(argv[1]).good() || !std::ifstream(argv[2]).good()) {
std::fprintf(stderr, "missing input file(s), skipping\n");
return 77;
}
int w = 0, h = 0, comp = 0;
unsigned char * rgba = stbi_load(argv[1], &w, &h, &comp, 4);
if (!rgba) {
std::fprintf(stderr, "decode failed: %s\n", stbi_failure_reason());
return 1;
}
int rw = 0, rh = 0, rc = 0;
unsigned char * ref = stbi_load(argv[2], &rw, &rh, &rc, 3);
if (!ref) {
std::fprintf(stderr, "decode failed: %s\n", stbi_failure_reason());
stbi_image_free(rgba);
return 1;
}
std::string err;
std::vector<uint8_t> got;
if (!trellis2_preprocess_rgba(rgba, w, h, rw, got, &err)) {
std::fprintf(stderr, "preprocess failed: %s\n", err.c_str());
return 1;
}
const size_t n = (size_t) rw * rh * 3;
size_t n_diff = 0, max_diff = 0, first = 0;
for (size_t i = 0; i < n; ++i) {
const int d = std::abs((int) got[i] - (int) ref[i]);
if (d) {
if (!n_diff) first = i;
++n_diff;
if ((size_t) d > max_diff) max_diff = (size_t) d;
}
}
std::printf("preprocess %dx%d -> %dx%d: %zu/%zu bytes differ (max %zu)\n",
w, h, rw, rh, n_diff, n, max_diff);
stbi_image_free(rgba);
stbi_image_free(ref);
// Byte-exact is the goal; tolerate nothing so regressions are loud.
if (n_diff != 0) {
std::printf("first diff at byte %zu\nRESULT: FAIL\n", first);
return 1;
}
std::printf("RESULT: PASS\n");
return 0;
}
+127
View File
@@ -0,0 +1,127 @@
#include "mesh_export.h"
#include "print_remesh.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <unordered_map>
#include <utility>
struct EdgeUse { int count = 0; int direction = 0; };
int main() {
if (!t2glb::print_remesh_available()) {
std::fprintf(stderr, "CGAL print-remesh test was built without its backend\n");
return 77;
}
// A single open, zero-thickness triangle is deliberately not printable.
// Alpha Wrap must enclose it in a closed volume despite having no usable
// source connectivity or inside/outside orientation.
const float verts[] = {0,0,0, 1,0,0, 0,1,0};
const int32_t tris[] = {0,1,2};
const float pbr[] = {
1,0,0,0,0.5f,1, 0,1,0,0,0.5f,1, 0,0,1,0,0.5f,1,
};
t2glb::MeshExportOptions opt;
opt.components = t2glb::ComponentFilter::KeepAll;
t2glb::PreparedMesh out;
std::string err;
if (!t2glb::prepare_print_mesh(verts, 3, tris, 1, pbr, opt,
0.20f, 0.03f, out, err)) {
std::fprintf(stderr, "prepare_print_mesh failed: %s\n", err.c_str());
return 1;
}
if (out.verts.empty() || out.tris.empty() || out.normals.size() != out.verts.size()) {
std::fprintf(stderr, "empty/incomplete wrap: %zu verts %zu tris %zu normals\n",
out.verts.size()/3, out.tris.size()/3, out.normals.size()/3);
return 1;
}
if (!out.pbr.empty()) {
std::fprintf(stderr, "new Alpha Wrap vertices incorrectly retained source PBR\n");
return 1;
}
// Closest-surface transfer must use barycentric interpolation on the source
// triangle, not nearest-vertex colors. The query is above (0.25,0.25,0),
// whose expected RGB weights are (0.5,0.25,0.25).
const std::vector<float> source_verts(verts, verts + 9);
const std::vector<int32_t> source_tris(tris, tris + 3);
const std::vector<float> source_pbr(pbr, pbr + 18);
const std::vector<float> queries = {0.25f, 0.25f, 0.5f};
std::vector<float> projected;
if (!t2print::project_pbr(source_verts, source_tris, source_pbr,
queries, projected, err) || projected.size() != 6) {
std::fprintf(stderr, "project_pbr failed: %s\n", err.c_str());
return 1;
}
const float expected[] = {0.5f, 0.25f, 0.25f, 0.0f, 0.5f, 1.0f};
for (int i = 0; i < 6; ++i) {
if (std::fabs(projected[i] - expected[i]) > 1e-5f) {
std::fprintf(stderr, "project_pbr channel %d: got %.7g expected %.7g\n",
i, projected[i], expected[i]);
return 1;
}
}
// Exercise the complete target unwrap -> per-texel source projection ->
// PBR PNG GLB path independently of the legacy T2GLB_XATLAS switch.
opt.texture_size = 64;
opt.dilate = 2;
std::vector<uint8_t> glb;
if (!t2glb::mesh_to_projected_glb(
out.verts.data(), (int) out.verts.size()/3,
out.tris.data(), (int) out.tris.size()/3,
verts, 3, tris, 1, pbr, opt, glb, err)) {
std::fprintf(stderr, "mesh_to_projected_glb failed: %s\n", err.c_str());
return 1;
}
if (glb.size() < 20 || std::memcmp(glb.data(), "glTF", 4) != 0) {
std::fprintf(stderr, "projected bake returned an invalid GLB\n");
return 1;
}
const std::string glb_bytes((const char *) glb.data(), glb.size());
if (glb_bytes.find("\"baseColorTexture\"") == std::string::npos ||
glb_bytes.find("\"metallicRoughnessTexture\"") == std::string::npos ||
glb_bytes.find("\"TEXCOORD_0\"") == std::string::npos ||
glb_bytes.find("\"COLOR_0\"") != std::string::npos) {
std::fprintf(stderr, "projected GLB is missing its UV PBR textures\n");
return 1;
}
std::unordered_map<uint64_t, EdgeUse> edges;
double signed_volume_6 = 0.0;
for (size_t t = 0; t < out.tris.size(); t += 3) {
const int32_t a = out.tris[t], b = out.tris[t+1], c = out.tris[t+2];
for (const auto e : {std::pair<int32_t,int32_t>{a,b}, {b,c}, {c,a}}) {
const uint32_t lo = (uint32_t) std::min(e.first, e.second);
const uint32_t hi = (uint32_t) std::max(e.first, e.second);
EdgeUse & use = edges[((uint64_t) lo << 32) | hi];
use.count++;
use.direction += e.first < e.second ? 1 : -1;
}
const float * va = out.verts.data() + (size_t) a * 3;
const float * vb = out.verts.data() + (size_t) b * 3;
const float * vc = out.verts.data() + (size_t) c * 3;
signed_volume_6 += va[0] * (vb[1]*vc[2] - vb[2]*vc[1])
+ va[1] * (vb[2]*vc[0] - vb[0]*vc[2])
+ va[2] * (vb[0]*vc[1] - vb[1]*vc[0]);
}
for (const auto & it : edges) {
if (it.second.count != 2 || it.second.direction != 0) {
std::fprintf(stderr, "non-manifold/inconsistently wound edge: count=%d direction=%d\n",
it.second.count, it.second.direction);
return 1;
}
}
if (std::fabs(signed_volume_6) < 1e-8) {
std::fprintf(stderr, "Alpha Wrap did not enclose a non-zero volume\n");
return 1;
}
std::printf("RESULT: PASS (%zu verts, %zu tris)\n",
out.verts.size()/3, out.tris.size()/3);
return 0;
}
+214
View File
@@ -0,0 +1,214 @@
// Validation of the shape-SLAT stage against the PyTorch reference
// (scripts/dump_slat_reference.py -> dumps/reference_slat.gguf):
//
// 1. SLAT flow forward at t=500 on the reference voxel set (tight gate)
// 2. full 12-step CFG sampler + denormalization (loose gate)
// 3. FDG decoder: per-level features, subdivision decisions,
// final 7-channel output + coords (tight gate,
// but only meaningful if the subdivision sets match exactly)
//
// usage: test_slat <slat_flow_f32.gguf> <shape_dec_f32.gguf> <reference_slat.gguf>
// exits 77 (ctest SKIP) when inputs are missing.
#include "trellis2.h"
#include "parity.hpp"
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <string>
#include <vector>
static bool file_exists(const std::string & p) {
std::ifstream f(p);
return f.good();
}
int main(int argc, char ** argv) {
if (argc < 4) {
std::fprintf(stderr,
"usage: %s <slat_flow_f32.gguf> <shape_dec_f32.gguf> <reference_slat.gguf>\n", argv[0]);
return 2;
}
const std::string flow_path = argv[1];
const std::string dec_path = argv[2];
const std::string ref_path = argv[3];
if (!file_exists(flow_path) || !file_exists(dec_path) || !file_exists(ref_path)) {
std::fprintf(stderr, "missing input file(s), skipping\n");
return 77;
}
t2_parity::baseline ref;
if (!ref.open(ref_path)) {
std::fprintf(stderr, "failed to open %s\n", ref_path.c_str());
return 1;
}
std::vector<float> coords_f, noise, cond;
if (!ref.load("coords", coords_f) || !ref.load("slat_noise", noise)) {
std::fprintf(stderr, "reference missing coords/slat_noise\n");
return 1;
}
const int L = (int) (coords_f.size() / 4);
std::vector<int32_t> coords((size_t) L * 3);
for (int v = 0; v < L; ++v) {
coords[(size_t) v * 3] = (int32_t) coords_f[(size_t) v * 4 + 1];
coords[(size_t) v * 3 + 1] = (int32_t) coords_f[(size_t) v * 4 + 2];
coords[(size_t) v * 3 + 2] = (int32_t) coords_f[(size_t) v * 4 + 3];
}
std::printf("reference: %d voxels\n", L);
// conditioning comes from the shared fixture
{
trellis2_dino_cond c;
std::string err;
const char * dinodata = std::getenv("TRELLIS2_DINODATA");
std::string path = dinodata ? dinodata : "dumps/fixture.dinodata";
if (!trellis2_load_dinodata(path, c, &err)) {
std::fprintf(stderr, "cannot load %s (%s), skipping\n", path.c_str(), err.c_str());
return 77;
}
cond = std::move(c.data);
}
const int Lkv = (int) (cond.size() / 1024);
std::string err;
trellis2_slat_flow_model * flow = trellis2_slat_flow_load(flow_path, true, &err);
if (!flow) { std::fprintf(stderr, "flow load failed: %s\n", err.c_str()); return 1; }
std::printf("flow backend: %s\n", trellis2_slat_flow_backend_name(flow));
int n_fail = 0;
// ── 1. forward parity at t=500 ───────────────────────────────────────────
{
std::vector<float> got((size_t) L * 32), want;
if (!trellis2_slat_flow_forward(flow, noise.data(), L, coords.data(), 500.0f,
cond.data(), Lkv, 1024, got.data(), &err)) {
std::fprintf(stderr, "flow forward failed: %s\n", err.c_str());
return 1;
}
ref.load("flow_t500_out", want);
// rel-L2 gate (matches the SS-flow tests): per-element noise from
// CPU-vs-CUDA fp32 reduction order is expected, the trajectory is not.
t2_parity::compare_stats st;
t2_parity::compare(got, want, "flow_t500_out", 2e-3, 2e-3, &st);
if (st.rel_l2 > 3e-3) { std::printf(" -> forward rel_l2 %.4g > 3e-3, FAIL\n", st.rel_l2); ++n_fail; }
}
// ── 2. full sampler + denormalization ────────────────────────────────────
std::vector<float> slat((size_t) L * 32);
{
trellis2_ss_sampler_params P;
P.steps = 12;
P.guidance_strength = 7.5f;
P.guidance_rescale = 0.5f;
P.guidance_interval_min = 0.6f;
P.guidance_interval_max = 1.0f;
P.rescale_t = 3.0f;
P.verbose = true;
if (!trellis2_slat_flow_sample(flow, L, coords.data(),
cond.data(), Lkv, 1024,
&P, noise.data(), /*denormalize*/ true,
slat.data(), &err)) {
std::fprintf(stderr, "flow sample failed: %s\n", err.c_str());
return 1;
}
std::vector<float> want;
ref.load("slat", want);
// The 12-step Euler sampler with CFG-rescale chaotically amplifies the
// per-step fp difference between backends (the same effect the SS
// sampler shows: tight on CPU, ~0.1 rel-L2 on GPU). Gate loosely on the
// trajectory here; the SS-sampler test already validates the shared
// Euler loop tightly on CPU. TRELLIS2_SLAT_STRICT tightens it for a CPU
// run.
t2_parity::compare_stats st;
t2_parity::compare(slat, want, "slat(sampled)", 5e-2, 5e-2, &st);
const double samp_gate = std::getenv("TRELLIS2_SLAT_STRICT") ? 2e-2 : 2e-1;
if (st.rel_l2 > samp_gate) {
std::printf(" -> sampler rel_l2 %.4g > %.0e, FAIL\n", st.rel_l2, samp_gate);
++n_fail;
}
// Decode the REFERENCE slat so decoder parity is independent of the
// sampler trajectory noise.
slat = want;
}
trellis2_slat_flow_free(flow);
// ── 3. decoder (CPU: ggml has no CUDA CONV_3D / sparse-conv kernel) ──────
trellis2_shape_dec_model * dec = trellis2_shape_dec_load(dec_path, true, &err, "cpu");
if (!dec) { std::fprintf(stderr, "dec load failed: %s\n", err.c_str()); return 1; }
std::printf("dec backend: %s\n", trellis2_shape_dec_backend_name(dec));
std::vector<float> out_feats;
std::vector<int32_t> out_coords;
std::vector<trellis2_subdiv_level> subs;
trellis2_shape_dec_taps taps;
if (!trellis2_shape_dec_decode_with_subs(dec, slat.data(), L, coords.data(),
out_feats, out_coords, subs, &taps, &err)) {
std::fprintf(stderr, "decode failed: %s\n", err.c_str());
trellis2_shape_dec_free(dec);
return 1;
}
trellis2_shape_dec_free(dec);
// Integrated texture generation replays these exact decoder decisions in
// the texture VAE. Guard their order/shape independently of activation taps.
if (subs.size() != 4) {
std::printf(" -> subdivision guide has %zu levels, expected 4, FAIL\n", subs.size());
++n_fail;
} else {
for (size_t lvl = 0; lvl < subs.size(); ++lvl) {
if (subs[lvl].fine_coords.size() != subs[lvl].cidx.size() * 3) {
std::printf(" -> subdivision level %zu coord/index size mismatch, FAIL\n", lvl);
++n_fail;
}
}
if (subs.back().fine_coords != out_coords) {
std::printf(" -> final subdivision guide does not reproduce decoder coords, FAIL\n");
++n_fail;
}
}
// The decoder's per-level active set is chosen by subdivision-logit signs;
// if my logits match the reference's, every level's voxel set (hence tap
// size) matches exactly. A size mismatch therefore means a real sign flip,
// which is a hard fail. Numerically, deep sparse-conv accumulates fp noise,
// so gate features on rel-L2 rather than per-element.
std::vector<float> refbuf;
int n_cmp = 0;
for (size_t i = 0; i < taps.names.size(); ++i) {
const std::string & name = taps.names[i];
if (!ref.has(name)) continue;
ref.load(name, refbuf);
++n_cmp;
t2_parity::compare_stats st;
t2_parity::compare(taps.data[i], refbuf, name, 2e-3, 2e-3, &st);
const double gate = 2e-2; // rel-L2
// A few voxels whose subdivision logit sits within fp-noise of zero
// flip the >0 threshold, so the final active set can differ by a
// handful out of millions. Tolerate a <0.05% set-size difference; a
// larger divergence is a real bug.
const size_t a = taps.data[i].size(), b = refbuf.size();
const size_t big = a > b ? a : b, sml = a > b ? b : a;
if (a != b) {
const double frac = (double) (big - sml) / (double) big;
if (frac > 5e-4) {
std::printf(" -> %s SIZE MISMATCH %zu vs %zu (%.3f%%), FAIL\n",
name.c_str(), a, b, 100.0 * frac);
++n_fail;
} else {
std::printf(" -> %s near-match (%zu vs %zu, %.4f%% subdivision boundary flip) OK\n",
name.c_str(), a, b, 100.0 * frac);
}
} else if (st.rel_l2 > gate) {
std::printf(" -> %s rel_l2 %.4g > %.0e, FAIL\n", name.c_str(), st.rel_l2, gate);
++n_fail;
}
}
std::printf("\ndecoder taps compared: %d, total failures: %d\n", n_cmp, n_fail);
std::printf("RESULT: %s\n", n_fail ? "FAIL" : "PASS");
return n_fail ? 1 : 0;
}
+100
View File
@@ -0,0 +1,100 @@
// test_ss_dec — validate the C++ SS decoder against the PyTorch reference
// produced by ref_ss_dec.py.
//
// usage: test_ss_dec <ss_dec_f32.gguf> <ss_dec_ref.bin> [rel_tol]
//
// Reads the input latent z_s and the reference occupancy logits, runs the C++
// decoder on the identical latent, and reports max abs / relative L2 error. Also
// checks sign agreement at 0 — the occupancy scaffold is logit>0, so the sign
// map is what ultimately matters. Default tolerance 3e-2.
#include "trellis2.h"
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>
static bool rd(std::ifstream & f, void * p, size_t n) {
return (bool) f.read(reinterpret_cast<char *>(p), (std::streamsize) n);
}
int main(int argc, char ** argv) {
if (argc < 3) {
std::fprintf(stderr, "usage: %s <ss_dec_f32.gguf> <ss_dec_ref.bin> [rel_tol]\n", argv[0]);
return 2;
}
const std::string gguf_path = argv[1], ref_path = argv[2];
{
std::ifstream _a(gguf_path), _b(ref_path);
if (!_a.good() || !_b.good()) {
std::fprintf(stderr, "missing input file(s), skipping\n");
return 77;
}
}
const double rel_tol = (argc > 3) ? std::atof(argv[3]) : 3e-2;
std::ifstream f(ref_path, std::ios::binary);
char magic[8];
if (!f || !rd(f, magic, 8) || std::memcmp(magic, "SSDEC001", 8) != 0) {
std::fprintf(stderr, "error: bad/missing ref file %s\n", ref_path.c_str());
return 1;
}
int32_t hdr[4];
rd(f, hdr, sizeof(hdr));
const int Cin = hdr[0], Rin = hdr[1], Oc = hdr[2], Rout = hdr[3];
const size_t n_in = (size_t) Cin * Rin * Rin * Rin;
const size_t n_out = (size_t) Oc * Rout * Rout * Rout;
std::vector<float> latent(n_in), ref(n_out);
rd(f, latent.data(), latent.size() * sizeof(float));
if (!rd(f, ref.data(), ref.size() * sizeof(float))) {
std::fprintf(stderr, "error: ref truncated\n");
return 1;
}
std::printf("ref: latent[%d,%d^3] -> logits[%d,%d^3]\n", Cin, Rin, Oc, Rout);
std::string err;
trellis2_ss_dec_model * m = trellis2_ss_dec_load(gguf_path, true, &err);
if (!m) { std::fprintf(stderr, "load error: %s\n", err.c_str()); return 1; }
std::printf("backend: %s\n", trellis2_ss_dec_backend_name(m));
const trellis2_ss_dec_hparams hp = trellis2_ss_dec_hparams_of(m);
if (hp.latent_channels != Cin || hp.res_in() != Rin || hp.out_channels != Oc || hp.res_out() != Rout) {
std::fprintf(stderr, "error: model/ref shape mismatch\n");
trellis2_ss_dec_free(m);
return 1;
}
std::vector<float> out(n_out, 0.0f);
if (!trellis2_ss_dec_decode(m, latent.data(), out.data(), &err)) {
std::fprintf(stderr, "decode error: %s\n", err.c_str());
trellis2_ss_dec_free(m);
return 1;
}
trellis2_ss_dec_free(m);
double max_abs = 0.0, sse = 0.0, ref_sq = 0.0;
size_t sign_agree = 0;
for (size_t i = 0; i < n_out; ++i) {
const double d = (double) out[i] - (double) ref[i];
max_abs = std::fmax(max_abs, std::fabs(d));
sse += d * d;
ref_sq += (double) ref[i] * (double) ref[i];
if ((out[i] > 0.0f) == (ref[i] > 0.0f)) ++sign_agree;
}
const double rel_l2 = std::sqrt(sse) / (std::sqrt(ref_sq) + 1e-30);
const double sign_pct = 100.0 * (double) sign_agree / (double) n_out;
std::printf("max abs err : %.3e\n", max_abs);
std::printf("rel L2 err : %.3e (tol %.1e)\n", rel_l2, rel_tol);
std::printf("sign agree : %.3f%% (occupancy is logit>0)\n", sign_pct);
if (rel_l2 > rel_tol) { std::printf("RESULT: FAIL\n"); return 1; }
std::printf("RESULT: PASS\n");
return 0;
}
+102
View File
@@ -0,0 +1,102 @@
// test_ss_flow_forward — validate the C++ SS-flow DiT forward pass against the
// PyTorch float32 reference produced by ref_ss_flow.py.
//
// usage: test_ss_flow_forward <ss_flow_dit_f32.gguf> <ss_flow_ref.bin> [rel_tol]
//
// Reads x/t/cond and the reference output from ss_flow_ref.bin, runs the C++
// forward with the f32 weights, and reports max abs / relative error. Exits
// nonzero if the relative L2 error exceeds the tolerance (default 2e-3).
#include "trellis2.h"
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>
static bool read_exact(std::ifstream & f, void * p, size_t n) {
return (bool) f.read(reinterpret_cast<char *>(p), (std::streamsize) n);
}
int main(int argc, char ** argv) {
if (argc < 3) {
std::fprintf(stderr, "usage: %s <f32.gguf> <ss_flow_ref.bin> [rel_tol]\n", argv[0]);
return 2;
}
const std::string gguf_path = argv[1];
const std::string ref_path = argv[2];
{
std::ifstream _a(gguf_path), _b(ref_path);
if (!_a.good() || !_b.good()) {
std::fprintf(stderr, "missing input file(s), skipping\n");
return 77;
}
}
const double rel_tol = (argc > 3) ? std::atof(argv[3]) : 2e-3;
// ── read the reference bundle ────────────────────────────────────────────
std::ifstream f(ref_path, std::ios::binary);
char magic[8];
if (!f || !read_exact(f, magic, 8) || std::memcmp(magic, "SSFREF01", 8) != 0) {
std::fprintf(stderr, "error: bad/missing ref file %s\n", ref_path.c_str());
return 1;
}
int32_t dims[5];
float t = 0.0f;
read_exact(f, dims, sizeof(dims));
read_exact(f, &t, sizeof(t));
const int R = dims[0], Cin = dims[1], Cout = dims[2], Lkv = dims[3], Cctx = dims[4];
const size_t N = (size_t) R * R * R;
std::vector<float> x( (size_t) Cin * N);
std::vector<float> cond((size_t) Lkv * Cctx);
std::vector<float> ref( (size_t) Cout * N);
read_exact(f, x.data(), x.size() * sizeof(float));
read_exact(f, cond.data(), cond.size() * sizeof(float));
if (!read_exact(f, ref.data(), ref.size() * sizeof(float))) {
std::fprintf(stderr, "error: ref file truncated\n");
return 1;
}
std::printf("ref: R=%d Cin=%d Cout=%d Lkv=%d Cctx=%d t=%.3f\n", R, Cin, Cout, Lkv, Cctx, t);
// ── load weights + run the C++ forward ───────────────────────────────────
std::string err;
trellis2_ss_flow_model * m = trellis2_ss_flow_load(gguf_path, /*load_tensors*/ true, &err);
if (!m) { std::fprintf(stderr, "load error: %s\n", err.c_str()); return 1; }
std::printf("backend: %s\n", trellis2_ss_flow_backend_name(m));
std::vector<float> out((size_t) Cout * N, 0.0f);
if (!trellis2_ss_flow_forward(m, x.data(), t, cond.data(), Lkv, Cctx, out.data(), &err)) {
std::fprintf(stderr, "forward error: %s\n", err.c_str());
trellis2_ss_flow_free(m);
return 1;
}
trellis2_ss_flow_free(m);
// ── compare ──────────────────────────────────────────────────────────────
double max_abs = 0.0, sse = 0.0, ref_sq = 0.0;
int max_i = 0;
for (size_t i = 0; i < out.size(); ++i) {
const double d = (double) out[i] - (double) ref[i];
if (std::fabs(d) > max_abs) { max_abs = std::fabs(d); max_i = (int) i; }
sse += d * d;
ref_sq += (double) ref[i] * (double) ref[i];
}
const double rel_l2 = std::sqrt(sse) / (std::sqrt(ref_sq) + 1e-30);
std::printf("out: min/max checked over %zu elems\n", out.size());
std::printf("max abs err : %.3e (at %d: cpp=%.6f ref=%.6f)\n",
max_abs, max_i, out[max_i], ref[max_i]);
std::printf("rel L2 err : %.3e (tol %.1e)\n", rel_l2, rel_tol);
if (rel_l2 > rel_tol) {
std::printf("RESULT: FAIL\n");
return 1;
}
std::printf("RESULT: PASS\n");
return 0;
}
+108
View File
@@ -0,0 +1,108 @@
// test_ss_sample — validate the C++ flow-Euler sampler against the PyTorch
// reference produced by ref_ss_sample.py.
//
// usage: test_ss_sample <ss_flow_dit_f32.gguf> <ss_sample_ref.bin> [rel_tol]
//
// Reads noise/cond/params and the reference latent, runs the C++ sampler with
// the same noise and settings, and reports max abs / relative L2 error. Also
// checks sign agreement (the decoder thresholds at 0, so the sign map is what
// ultimately matters). Default tolerance 3e-2.
#include "trellis2.h"
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>
static bool rd(std::ifstream & f, void * p, size_t n) {
return (bool) f.read(reinterpret_cast<char *>(p), (std::streamsize) n);
}
int main(int argc, char ** argv) {
if (argc < 3) {
std::fprintf(stderr, "usage: %s <f32.gguf> <ss_sample_ref.bin> [rel_tol]\n", argv[0]);
return 2;
}
const std::string gguf_path = argv[1], ref_path = argv[2];
{
std::ifstream _a(gguf_path), _b(ref_path);
if (!_a.good() || !_b.good()) {
std::fprintf(stderr, "missing input file(s), skipping\n");
return 77;
}
}
const double rel_tol = (argc > 3) ? std::atof(argv[3]) : 3e-2;
std::ifstream f(ref_path, std::ios::binary);
char magic[8];
if (!f || !rd(f, magic, 8) || std::memcmp(magic, "SSSAMP01", 8) != 0) {
std::fprintf(stderr, "error: bad/missing ref file %s\n", ref_path.c_str());
return 1;
}
int32_t hdr[5];
float pf[6];
rd(f, hdr, sizeof(hdr));
rd(f, pf, sizeof(pf));
const int R = hdr[0], Cin = hdr[1], Lkv = hdr[2], Cctx = hdr[3], steps = hdr[4];
const size_t N = (size_t) R * R * R;
const size_t n = (size_t) Cin * N;
std::vector<float> noise(n), cond((size_t) Lkv * Cctx), ref(n);
rd(f, noise.data(), noise.size() * sizeof(float));
rd(f, cond.data(), cond.size() * sizeof(float));
if (!rd(f, ref.data(), ref.size() * sizeof(float))) {
std::fprintf(stderr, "error: ref truncated\n");
return 1;
}
trellis2_ss_sampler_params P;
P.steps = steps;
P.guidance_strength = pf[0];
P.guidance_rescale = pf[1];
P.guidance_interval_min = pf[2];
P.guidance_interval_max = pf[3];
P.rescale_t = pf[4];
P.sigma_min = pf[5];
P.verbose = true;
std::printf("ref: R=%d Cin=%d Lkv=%d steps=%d gs=%.2f rescale=%.2f interval=[%.2f,%.2f] rescale_t=%.1f\n",
R, Cin, Lkv, steps, P.guidance_strength, P.guidance_rescale,
P.guidance_interval_min, P.guidance_interval_max, P.rescale_t);
std::string err;
trellis2_ss_flow_model * m = trellis2_ss_flow_load(gguf_path, true, &err);
if (!m) { std::fprintf(stderr, "load error: %s\n", err.c_str()); return 1; }
std::printf("backend: %s\n", trellis2_ss_flow_backend_name(m));
std::vector<float> out(n, 0.0f);
if (!trellis2_ss_flow_sample(m, cond.data(), Lkv, Cctx, &P, noise.data(), out.data(), &err)) {
std::fprintf(stderr, "sample error: %s\n", err.c_str());
trellis2_ss_flow_free(m);
return 1;
}
trellis2_ss_flow_free(m);
double max_abs = 0.0, sse = 0.0, ref_sq = 0.0;
size_t sign_agree = 0;
for (size_t i = 0; i < n; ++i) {
const double d = (double) out[i] - (double) ref[i];
max_abs = std::fmax(max_abs, std::fabs(d));
sse += d * d;
ref_sq += (double) ref[i] * (double) ref[i];
if ((out[i] > 0.0f) == (ref[i] > 0.0f)) ++sign_agree;
}
const double rel_l2 = std::sqrt(sse) / (std::sqrt(ref_sq) + 1e-30);
const double sign_pct = 100.0 * (double) sign_agree / (double) n;
std::printf("max abs err : %.3e\n", max_abs);
std::printf("rel L2 err : %.3e (tol %.1e)\n", rel_l2, rel_tol);
std::printf("sign agree : %.3f%% (decoder thresholds z_s at 0)\n", sign_pct);
if (rel_l2 > rel_tol) { std::printf("RESULT: FAIL\n"); return 1; }
std::printf("RESULT: PASS\n");
return 0;
}
+242
View File
@@ -0,0 +1,242 @@
// Validation of the PBR-texture stages against the PyTorch reference
// (scripts/dump_texture_reference.py -> dumps/reference_texture.gguf):
//
// 1. shape encoder: dual grid -> shape SLAT (32ch) (coord-matched)
// 2. tex flow forward at t=500 with concat_cond (tight gate)
// 3. tex flow full sampler + denorm (loose gate)
// 4. tex decoder: tex SLAT -> 6ch PBR, replaying the (coord-matched)
// encoder's subdivision
//
// usage: test_texture <shape_enc.gguf> <tex_flow_512.gguf> <tex_dec.gguf> <reference_texture.gguf>
// exits 77 (ctest SKIP) when inputs are missing.
#include "trellis2.h"
#include "parity.hpp"
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <string>
#include <unordered_map>
#include <vector>
static bool file_exists(const std::string & p) { std::ifstream f(p); return f.good(); }
static uint64_t vkey(int32_t x, int32_t y, int32_t z) {
return ((uint64_t) (uint32_t) x << 42) | ((uint64_t) (uint32_t) y << 21) | (uint64_t) (uint32_t) z;
}
// coords4 is the reference [N,4] (batch,x,y,z) dump; build key->row.
static std::unordered_map<uint64_t,int> coord_map(const std::vector<float> & c4) {
std::unordered_map<uint64_t,int> m;
const int n = (int) (c4.size() / 4);
m.reserve((size_t) n * 2);
for (int v = 0; v < n; ++v)
m[vkey((int32_t) c4[(size_t) v*4+1], (int32_t) c4[(size_t) v*4+2], (int32_t) c4[(size_t) v*4+3])] = v;
return m;
}
// Reorder `got` (rows in `got_coords3` order) into the reference row order given
// by `ref_coords4`, returning the aligned buffer; reports set mismatches.
static bool align_to_ref(const std::vector<float> & got, const std::vector<int32_t> & got_coords3,
const std::vector<float> & ref_coords4, int ch,
std::vector<float> & out, const char * label) {
const int ng = (int) (got_coords3.size() / 3);
const int nr = (int) (ref_coords4.size() / 4);
if (ng != nr) {
std::printf("[%-16s] VOXEL COUNT got=%d ref=%d -> FAIL\n", label, ng, nr);
return false;
}
std::unordered_map<uint64_t,int> gm;
gm.reserve((size_t) ng * 2);
for (int v = 0; v < ng; ++v)
gm[vkey(got_coords3[(size_t) v*3], got_coords3[(size_t) v*3+1], got_coords3[(size_t) v*3+2])] = v;
out.resize((size_t) nr * ch);
int miss = 0;
for (int r = 0; r < nr; ++r) {
auto it = gm.find(vkey((int32_t) ref_coords4[(size_t) r*4+1],
(int32_t) ref_coords4[(size_t) r*4+2],
(int32_t) ref_coords4[(size_t) r*4+3]));
if (it == gm.end()) { ++miss; continue; }
std::memcpy(out.data() + (size_t) r * ch, got.data() + (size_t) it->second * ch, (size_t) ch * sizeof(float));
}
if (miss) { std::printf("[%-16s] %d/%d ref voxels absent in got -> FAIL\n", label, miss, nr); return false; }
return true;
}
int main(int argc, char ** argv) {
if (argc < 5) {
std::fprintf(stderr, "usage: %s <shape_enc.gguf> <tex_flow_512.gguf> <tex_dec.gguf> <reference_texture.gguf>\n", argv[0]);
return 2;
}
const std::string enc_path = argv[1], flow_path = argv[2], dec_path = argv[3], ref_path = argv[4];
if (!file_exists(enc_path) || !file_exists(flow_path) || !file_exists(dec_path) || !file_exists(ref_path)) {
std::fprintf(stderr, "missing input file(s), skipping\n");
return 77;
}
t2_parity::baseline ref;
if (!ref.open(ref_path)) { std::fprintf(stderr, "failed to open %s\n", ref_path.c_str()); return 1; }
std::vector<float> enc_vert, enc_inter, enc_coords4, cond;
std::vector<float> shape_slat_ref, shape_coords4, tex_noise, tex_slat_ref, pbr_ref, pbr_coords4;
if (!ref.load("enc_vert", enc_vert) || !ref.load("enc_inter", enc_inter) ||
!ref.load("enc_coords", enc_coords4) || !ref.load("cond", cond) ||
!ref.load("shape_slat", shape_slat_ref) || !ref.load("shape_coords", shape_coords4) ||
!ref.load("tex_noise", tex_noise) || !ref.load("tex_slat", tex_slat_ref) ||
!ref.load("pbr", pbr_ref) || !ref.load("pbr_coords", pbr_coords4)) {
std::fprintf(stderr, "reference missing a required tensor\n");
return 1;
}
const int N = (int) (enc_coords4.size() / 4); // fine voxels (encoder input)
const int Nl = (int) (shape_coords4.size() / 4); // latent voxels
const int Lkv = (int) (cond.size() / 1024);
std::printf("reference: %d dual-grid voxels, %d latent voxels, %d cond tokens\n", N, Nl, Lkv);
std::vector<int32_t> enc_coords((size_t) N * 3);
std::vector<float> in6((size_t) N * 6);
for (int v = 0; v < N; ++v) {
enc_coords[(size_t) v*3] = (int32_t) enc_coords4[(size_t) v*4+1];
enc_coords[(size_t) v*3+1] = (int32_t) enc_coords4[(size_t) v*4+2];
enc_coords[(size_t) v*3+2] = (int32_t) enc_coords4[(size_t) v*4+3];
for (int c = 0; c < 3; ++c) {
in6[(size_t) v*6 + c] = enc_vert[(size_t) v*3 + c];
in6[(size_t) v*6 + 3 + c] = enc_inter[(size_t) v*3 + c];
}
}
std::vector<int32_t> lat_coords((size_t) Nl * 3);
for (int v = 0; v < Nl; ++v) {
lat_coords[(size_t) v*3] = (int32_t) shape_coords4[(size_t) v*4+1];
lat_coords[(size_t) v*3+1] = (int32_t) shape_coords4[(size_t) v*4+2];
lat_coords[(size_t) v*3+2] = (int32_t) shape_coords4[(size_t) v*4+3];
}
std::string err;
int n_fail = 0;
// ── 1. shape encoder ─────────────────────────────────────────────────────
trellis2_shape_enc_model * enc = trellis2_shape_enc_load(enc_path, true, &err);
if (!enc) { std::fprintf(stderr, "enc load failed: %s\n", err.c_str()); return 1; }
std::printf("enc backend: %s\n", trellis2_shape_enc_backend_name(enc));
std::vector<float> shape_slat;
std::vector<int32_t> shape_coords;
std::vector<trellis2_subdiv_level> subs;
if (!trellis2_shape_enc_encode(enc, in6.data(), N, enc_coords.data(),
shape_slat, shape_coords, subs, nullptr, &err)) {
std::fprintf(stderr, "encode failed: %s\n", err.c_str());
trellis2_shape_enc_free(enc); return 1;
}
trellis2_shape_enc_free(enc);
std::printf("encoder out: %zu latent voxels\n", shape_coords.size() / 3);
{
std::vector<float> aligned;
if (!align_to_ref(shape_slat, shape_coords, shape_coords4, 32, aligned, "shape_slat")) {
++n_fail;
} else {
t2_parity::compare_stats st;
t2_parity::compare(aligned, shape_slat_ref, "shape_slat", 5e-3, 5e-3, &st);
if (st.rel_l2 > 3e-2) { std::printf(" -> shape_slat rel_l2 %.4g > 3e-2, FAIL\n", st.rel_l2); ++n_fail; }
}
}
// ── 2 & 3. tex flow (concat_cond) ────────────────────────────────────────
trellis2_slat_flow_model * flow = trellis2_slat_flow_load(flow_path, true, &err);
if (!flow) { std::fprintf(stderr, "flow load failed: %s\n", err.c_str()); return 1; }
const trellis2_slat_flow_hparams & fhp = trellis2_slat_flow_hparams_of(flow);
std::printf("flow backend: %s (in=%d out=%d concat=%d)\n", trellis2_slat_flow_backend_name(flow),
fhp.in_channels, fhp.out_channels, fhp.concat_cond_channels);
// 2. forward @ t=500: build [noise(32) | normalized shape_slat(32)] manually.
{
std::vector<float> xin((size_t) Nl * fhp.in_channels), got((size_t) Nl * 32), want;
for (int v = 0; v < Nl; ++v) {
for (int c = 0; c < 32; ++c) xin[(size_t) v*64 + c] = tex_noise[(size_t) v*32 + c];
for (int c = 0; c < 32; ++c)
xin[(size_t) v*64 + 32 + c] =
(shape_slat_ref[(size_t) v*32 + c] - fhp.concat_norm_mean[c]) / fhp.concat_norm_std[c];
}
if (!trellis2_slat_flow_forward(flow, xin.data(), Nl, lat_coords.data(), 500.0f,
cond.data(), Lkv, 1024, got.data(), &err)) {
std::fprintf(stderr, "flow forward failed: %s\n", err.c_str());
return 1;
}
ref.load("tex_flow_t500", want);
t2_parity::compare_stats st;
t2_parity::compare(got, want, "tex_flow_t500", 2e-3, 2e-3, &st);
if (st.rel_l2 > 5e-3) { std::printf(" -> forward rel_l2 %.4g > 5e-3, FAIL\n", st.rel_l2); ++n_fail; }
}
// 3. full tex sampler.
{
// Texture sampler params (texturing_pipeline.json tex_slat_sampler):
// guidance_strength 1.0 (no CFG amplification), rescale 0, interval [0.6,0.9].
trellis2_ss_sampler_params P;
P.steps = 12; P.guidance_strength = 1.0f; P.guidance_rescale = 0.0f;
P.guidance_interval_min = 0.6f; P.guidance_interval_max = 0.9f; P.rescale_t = 3.0f;
P.verbose = true;
std::vector<float> got((size_t) Nl * 32), want;
if (!trellis2_slat_flow_sample_tex(flow, Nl, lat_coords.data(), cond.data(), Lkv, 1024,
shape_slat_ref.data(), &P, tex_noise.data(),
/*denormalize*/ true, got.data(), &err)) {
std::fprintf(stderr, "tex sample failed: %s\n", err.c_str());
return 1;
}
ref.load("tex_slat", want);
t2_parity::compare_stats st;
t2_parity::compare(got, want, "tex_slat(sampled)", 5e-2, 5e-2, &st);
const double gate = std::getenv("TRELLIS2_SLAT_STRICT") ? 2e-2 : 2e-1;
if (st.rel_l2 > gate) { std::printf(" -> tex sampler rel_l2 %.4g > %.0e, FAIL\n", st.rel_l2, gate); ++n_fail; }
}
trellis2_slat_flow_free(flow);
// ── 4. tex decoder: decode the REFERENCE tex SLAT (isolate from sampler) ──
// Align the reference tex SLAT into the encoder's latent order, then decode
// with the encoder's subdivisions.
std::vector<float> tex_slat_aligned;
if (!align_to_ref(tex_slat_ref, lat_coords, shape_coords4, 32, tex_slat_aligned, "tex_slat_align")) {
// reference latent order should already match ours; if not, fail loudly
std::printf(" -> could not align tex SLAT to encoder latent order, FAIL\n");
return ++n_fail, (n_fail ? 1 : 0);
}
// aligned is in reference order; but the decoder wants it in the encoder's
// order (shape_coords). Reorder reference->encoder via the encoder coords.
std::vector<float> tex_slat_enc((size_t) Nl * 32);
{
std::unordered_map<uint64_t,int> rm = coord_map(shape_coords4);
for (int v = 0; v < Nl; ++v) {
auto it = rm.find(vkey(shape_coords[(size_t) v*3], shape_coords[(size_t) v*3+1], shape_coords[(size_t) v*3+2]));
const int rr = (it == rm.end()) ? v : it->second;
std::memcpy(tex_slat_enc.data() + (size_t) v*32, tex_slat_ref.data() + (size_t) rr*32, 32*sizeof(float));
}
}
trellis2_shape_dec_model * texdec = trellis2_tex_dec_load(dec_path, true, &err);
if (!texdec) { std::fprintf(stderr, "tex_dec load failed: %s\n", err.c_str()); return 1; }
std::printf("tex_dec backend: %s\n", trellis2_shape_dec_backend_name(texdec));
std::vector<float> pbr;
std::vector<int32_t> pbr_coords;
if (!trellis2_tex_dec_decode(texdec, tex_slat_enc.data(), Nl, shape_coords.data(),
subs, pbr, pbr_coords, &err)) {
std::fprintf(stderr, "tex decode failed: %s\n", err.c_str());
trellis2_shape_dec_free(texdec); return 1;
}
trellis2_shape_dec_free(texdec);
std::printf("tex decode out: %zu PBR voxels\n", pbr_coords.size() / 3);
{
std::vector<float> aligned;
if (!align_to_ref(pbr, pbr_coords, pbr_coords4, 6, aligned, "pbr")) {
++n_fail;
} else {
t2_parity::compare_stats st;
t2_parity::compare(aligned, pbr_ref, "pbr", 5e-3, 5e-3, &st);
if (st.rel_l2 > 3e-2) { std::printf(" -> pbr rel_l2 %.4g > 3e-2, FAIL\n", st.rel_l2); ++n_fail; }
}
}
std::printf("\nRESULT: %s (failures: %d)\n", n_fail ? "FAIL" : "PASS", n_fail);
return n_fail ? 1 : 0;
}