Imported from V-Sekai-fire/interactor-dress-on (
AGENTS.md). Install upstream withnpx skills add V-Sekai-fire/interactor-dress-on. Copyright stays with the author.
AGENTS.md — how to work in interactor-dress-on
Standing rules for any agent (or person) touching this repo. Amend this file when a rule is added or changed; the plan and READMEs cite it, they do not duplicate it.
What this is
A garment authoring loop for Godot: image → curvenet → dress-on → drape, as
godot-sandbox RISC-V guest ELFs that require only the godot_sandbox
addon and GPU rendering. No engine fork, no engine module, no GDExtension of
our own, no host DLL. The GPU is reachable only through Godot's
RenderingDevice, via guest/rd_compute (Stage 1, measured).
Rules
- Stay inside github.com/V-Sekai-fire. Every dependency comes from the
org's repo or fork (
ggml,cloth-fit,pixal3d-ggml,entities-godot,godot-sandbox,transport-xr-grid,transport-godot-mcp,interactor-mujoco-sandbox-demo,skin-tokens-ggml; forlean/:contract-lean-slang,plausible,plausible-witness-dag). If only aV-Sekai/or upstream copy exists, ask before forking it in. Push only to org remotes. - One source, two targets: Lean → Slang →
cpp|spirv. Kernels are generated fromlean/(a squashed git subtree of cloth-dynamics'lean/, cited inlean/CITATION.cff) bylake exe emit_shaders(kernels/avbd/gen.sh;CLOTH_LEANoverrides the path);slangc -target cppis the in-guest CPU path (AvbdCpu),slangc -target spirvis the GPU path (AvbdRdoverrd_compute). Never copy prebuilt kernels in; never hand-write a kernel that Lean could emit. Optimizers too: L-BFGS-B is written in Lean→Slang, not pulled in as a library. - No Eigen in the drape ELF. The drape is Lean-generated kernels plus an
Eigen-free driver (
guest/avbd/avbd_sim.hlineage). Eigen survives only insidefit.elf(PolyFEM), a separate ELF. - State machines and queues, not waits. Never
sync()in the frame thatsubmit()s; the host advances each guest's state machine from_processand reads back once the fence is known-done. Batch iterations into one compute list (AvbdRd::run). - Batch / CPU / GPU chosen per problem. Small meshes run on
AvbdCpu, large ones onAvbdRd; thresholds come from a measured gate, never a guess. - Composition over one monolith. One ELF per stage in its own Sandbox
node (
curvenet,infer,fit,drape), composed byproject/main.gd; meshes cross through the host as packed arrays;rd_computeis a static lib they share. - Gates before building on assumptions. Anything that could void the plan
gets a runnable gate under
gates/with its logs kept as evidence, a README that states the result (including negative ones), and a flat control that separates "the sandbox/VR blocked it" from "nothing was there". - Every guest entry point gets a no-argument wrapper in
project/main.gdso MCPcall_methodneeds no argument marshalling (Gate 0E). main.gd is a thin root: the wrapper lives in the stage file and main.gd keeps a same-named delegate with the same defaults;tests/probe_main_wrappers.gdFAILs on anyADD_API_FUNCTIONwithout one. - Do not touch the user's machine config. OpenXR runtime is selected per
process with
XR_RUNTIME_JSON(OXRSys, Windows port, attools/oxrsys/build/windows/runtime/oxrsys-runtime.json; its Qt simulator shows the stream); the system default and SteamVR's settings are not ours to flip.
Facts that cost time (do not relearn)
- Run Godot with
--rendering-driver vulkan;--headlesshands back a null RenderingDevice.--xr-mode offon non-VR runs. - Godot buffers stdout when redirected: poll the port, or write results to
a file from GDScript; never wait on the log.
--quit-afternever fires under--xr-mode on; quit on a wall clock, in every branch. - RenderingDevice enums are pinned by hand in
guest/rd_enums.h(SHADER_STAGE_COMPUTE=4,UNIFORM_TYPE_UNIFORM_BUFFER=7,UNIFORM_TYPE_STORAGE_BUFFER=8).RID::RID(const Variant&)links only while unused; usev.operator ::RID(). - The render graph does not order same-buffer dispatches inside one
compute list;
compute_list_add_barrieris mandatory between dependent dispatches (it isend()+begin()+rebind, there is no cheaper form). - Each span between barriers is one graph command, and it keeps a buffer's first usage only (release builds drop a second one silently). A buffer first bound read-only and then written in the same span is recorded as read-only, and later spans race its writes (Gate 0F finding 4: 0 of 4096 exact). Bind a written buffer read-write first; never alias a read-only source binding with the read-write destination for an in-place op.
buffer_updateis refused inside a compute list;RDUniformbinds whole buffers; every buffer is created with contents (zeros if none), exceptrdc::Device::storage_buffer_uninitfor sizes a zero array cannot reach (4 GiB−256), which must bebuffer_cleared.buffer_get_datastages the whole buffer; read big buffers throughbuffer_copyinto a small one.- A guest static can hold the RenderingDevice across vmcalls (handle = engine instance id in unrestricted mode); RefCounted helpers are per-call only.
- An RID (any handle a host call returns) is a per-vmcall scoped Variant: the
guest holds an index into that call's Variant table, which names something
else in the next call. Anything kept across vmcalls must be made permanent
(
Variant::make_permanent, asrdc::Devicedoes); permanent slots are min(references_max + guest globals, 65534) and must be freed (ECALL_VSTORE_GLOBAL,Device::forget). - The guest clock is not a clock (it jumps between time bases). Time on the host, around the vmcall.
Sandbox.references_maxdefaults to 100; ~30 uniform sets in one call trip it. Host scripts set 4096 (or more). It only grows: a lower value set later reads back but is not applied. Recording on permanent RIDs uses none.Sandbox.memory_max(MiB, default 512): the heap is 0.8 x it and must end below 4 GiB, so ~5112 is the most that loads (~4088 MiB of heap). Set it beforeprogram=; afterprogram=only a larger value is applied.- The guest has no filesystem (
openat→ EBADF); every byte comes through the host.fesetroundis accepted and ignored. slangc -preserve-paramskeeps unused bindings only at-O0; at the default-O1the optimiser strips them again (Gate 0F finding 9).- godot-sandbox caches an Object call's method name in a 32-slot direct-mapped
cache keyed by the guest ADDRESS of the name string; two hot names in one
slot evict each other and each call re-resolves (~2-5 ms). It is decided
by the link layout, so it moves between builds, not processes: the old
"bimodal submit+sync" (~70 µs or ~2.4 ms) reproduces as 62-174 µs vs
2.4-2.8 ms per submit with
compute_list_endandsyncin one slot, and it was the 50-86x rd regression of Cut A (gates/2-avbd/perf-bisect.log). Call RenderingDevice only throughrdc::Device, whose method names sit at addresses with a slot each. - Cross-compile:
build.sh(riscv64 clang from scoop, lld, the org'sriscv64-sysrootviaRISCV64_SYSROOT). First Godot run after adding an ELF:godot --path project --headless --import. lean/is a subtree. To take upstream changes, split in a scratch clone of cloth-dynamics (git subtree split --prefix=lean -b lean-split, ~2 min for 425 commits; the clone keeps the split refs out of the real checkout), thengit subtree pull --prefix=lean <clone> lean-split --squashhere. Keep the V-Sekai-fire URLs inlean/lakefile.leanandlake-manifest.jsonwhen resolving.lake buildfrom an emptylean/.laketakes 50-134 s (three packages fetched and built;gates/lean/);lean/.lake/is ignored. Aftergates/lean/verify.sh,git statusmust show only its logs.- LeanSlang is
V-Sekai-fire/contract-lean-slang(the renamedlean-slang; the old URL redirects, but pin the canonical one) at branchemit-fp, pinned by SHA (60532ae), notmain.emit-fpis v0.0.6 plushalf,double,litHalf,litInt,castandlitFloatExact/litDoubleExact, additive, so the AVBD emission is byte-identical.litFloatprints six decimals (1e-12 emits as 0.000000): any literal that is not a multiple of 1e-6 must uselitFloatExact(binary32) orlitDoubleExact.mainadds a libslang FFIextern_libas a default target (vendored SDK headers, Linux link flags) that breakslake exeon Windows. Changing the URL: deletelean/.lake/packages/LeanSlangfirst, thenlake update LeanSlang(only that package; the other revs must not move). - The guest heap also caps live allocations:
Sandbox.allocations_maxdefaults to 10000 ("Too many arena chunks"). fit.elf holds ~79k afterfit_begin;stages/fit_stage.gdsets 4,000,000 beforeprogram=. - Unqualified
abs(double)binds to C'sint absunder the guest's libstdc++ (clang-Wabsolute-value) but to the double overload under llvm-mingw's libc++, so native and guest silently differ. Treat that warning as an error in anything shared between them. - libstdc++ (guest) and libc++ (native) leave tied sort keys in different
orders (
std::sort,nth_element,partial_sort), and a sum taken in that order differs by one ULP. fit.elf and fit_native part after 7 Newton iterations while inputs, LDLT and the libm the solver calls are bitwise equal (gates/6-fit, 6.0); SimpleBVH's Morton sort, which has such ties, is the likely cause (hypothesis: no call site instrumented yet). Test an index tie-break before relying on it. Confirmed at one call site: Geogram's Hilbert sort (nth_elementon one coordinate) inserted a skirt panel's tied boundary points in another order, DMWT tiled the panel differently, and an index tie-break inHilbert_vcmpmade curvenet.elf bit-identical to its native control again (gates/4-curvenet/README.md, skirt (c)). - Guest out-of-memory is not
std::bad_alloc. Below the heap floor a failed allocation is aProtection faultat the malloc ecall (the vmcall aborts) or a segfault that kills Godot (exit 139), and the heap's meminfo cannot see in-phase peaks. Sizememory_maxwith a ladder of fresh Sandboxes, then add 1.25x headroom. For fit.elf on foxgirl the floor is 352 MiB, run at 440. execution_timeout(2^20-instruction units) must cover a whole vmcall. One fit phase is up to 5.4e5 units, 67x the 8000 default. Read a call's instructions from the guest withrdinstret; libriscv counts from the vmcall's start.- Starting several Godot processes in the same second segfaulted one once. Stagger launches by a few seconds.
- Godot imports every
.objunderres://as a mesh and fails on line-only ones (skeletons). Data OBJs live under a.gdignored directory (project/fixtures/) and are read as text (util/obj_io.gd). - Make a stage's Sandbox with
stages/sandbox_util.gd(memory_max, references_max, execution_timeout beforeprogram=; a missing ELF or entry point is a reason, not an error). In XR the root viewport reads back black: screenshot a SubViewport on the same World3D. - The GPU and the guest CPU round the same Slang differently (the driver forms FMAs slangc's cpp build does not), so a sum at float noise can be exactly 0 on rd and not on cpu. Guard every division by a computed norm in the Lean kernel (Gate 5 G10: a bending hinge went NaN on rd only).
- Bash heredocs with apostrophes and long scripts fail in this harness; write scripts with the Write tool and run them.
- godot-sandbox's guest heap has no aligned entry point (malloc/calloc/
realloc/free are syscalls into a host heap that keeps its bookkeeping
outside guest memory and hands out 16-byte alignment). Upstream's
memalignfallback (behindposix_memalign,aligned_alloc, alignednew) returned an already-freed block whenever 16 malloc tries missed a16-byte alignment: nearly always at 4096, sometimes at 64 (Gate 4's "Possible double-free" in Geogram). Fixed in
vendor/sandbox-api'snative.cpp: over-allocate, return the aligned address, and map it back to the host block in a side table that the wrappedfree/reallocconsult (a header below the block cannot work: the host only frees the pointer it returned). Gate 0F probe 17 checks it; re-vendoring sandbox-api must keep that patch, or the bug returns silently. - A native flat control built with llvm-mingw links libc++; the guest links
libstdc++.
std::shuffleandstd::uniform_*_distributiondiffer between them from the same seed: use the engine's raw output (Gate 4). - In a guest, a
std::vector<std::vector<T>>kept alive in a long-lived record (astd::vectorof records, across stages) corrupted libc state: "Illegal opcode" at anecallin memmove/memcpy/puts/fflush, garbage syscall numbers. As a local it is fine, and ASan on the host is clean. Keep long-lived guest data flat (gates/5-drape/drape/README.md). Godot's--gpu-indexorder is not stable (index 1 was the RTX 4090 on one boot, index 0 on the next): check the adapter line in the log. The native references ran on the 4090; the drape is bit-identical on the 3090. - DiffCloth's sphere demo is chaotic after its self-collision onset (step
~70): a 9.7e-9 change of mu (mu0 vs float32(mu0)) moves the 350-step
dL/dmu by 4.4%, and a 4.4e-9 change moves it by 7.8% and the loss by 6.5%.
Compare with the native run at its exact seed-1 mu, 0.5397701956236457
(
kNativeSphereMu0), never the printed 0.539770; at the exact value every printed per-step statistic matches native to step 70 (gates/5-drape/trace). - An L-BFGS-B guard that LBFGSpp writes with
numeric_limits<double>::epsilon()as an absolute threshold keeps 2^-52 in the float32 kernels (dblEps), not FLT_EPSILON: the sphere demo's gradients are ~1e-5, so fpp = g.g ~ 1e-10, and FLT_EPSILON there shrank the first Cauchy step to nothing.
Conventions
- Commit messages: plain prose, five whys, the numbers. No Claude
annotations — no
Co-Authored-By/Claude-Sessiontrailers, no assistant attributions in files. - Plan of record:
~/.claude/plans/declarative-soaring-cake.md(the user's plan file); status table at its top. Amend it when a decision changes. - Vendored code carries a
CITATION.cffin the org's form (title, abstract, authors, repository-code, commit, license) naming the source and the local adaptations; never a PROVENANCE.txt.