Repository navigation
M4.05
Pre-release
Pre-release
[M4.05] — 2026-10-18/20
The freeze-review + C-ABI + frame-path wave: the freeze-review findings
land as code and contract changes (process-global glslang frontend mutex,
Chain::resize dead parameters dropped, docs/API.md folded to match the
delivered surface), the library ships its first foreign-ABI surface — a
pure-C header, adapter and headless test suite — and the per-frame Vulkan
path drops its second submit + fence-drain cycle by folding the host
upload into the chain's own frame submit. Closes with the fix that keeps
the public header's only external dependency on the Vulkan headers.
Added
- C API (
include/slangvk/c.h+src/c_api.cpp) — the runner's first
foreign-ABI surface: a pure-C header (stddef.h/stdint.h+
vulkan/vulkan.honly,extern "C", consumable from any FFI language)
mirroring the C++ API 1:1.slangvk_resultmirrorsslv::Errc
code-for-code,slangvk_result_name()names codes for logs and
thread-localslangvk_last_error()carries the last failed call's
message; opaqueslangvk_device/slangvk_image/slangvk_preset/
slangvk_chainhandles; 25 entry points covering devicecreate
(library-owned) oradopt(host-owned instance/device/queue — the
DevilutionX mode) plusget_handles/get_info/wait_idle/
read_pixels, imagecreate/update/get_handles, presetload+
pass/parameter queries, and chaincreate/set_parameter/resize/
render_swapchain/render_offscreen/pass_output_size— with the C++
contracts carried over verbatim (input layout, preset outlives chains,
single-threaded chains, resize before render after an extent change).
slangvk.hpppulls the header in; bothslangvkandslangvk-shared
build the adapter.tests/test_c_api.cpp(424 lines) drives the
create → load → render → destroy loop headless at 64×64 and exercises
the error model, skipping cleanly (exit 0) when no Vulkan ICD is
present — CI runs it on lavapipe, Windows CI has none. - Single-submit per-frame upload path (
Device::createMappedBuffer/
destroyMappedBuffer,InputFrame::pixelBuffer) — hosts doing their own
per-frame upload no longer pay the standaloneImage::updateround-trip:
its own staging alloc/free + command buffer + submit + fence drain on top
of the chain's render submit.createMappedBuffer()allocates a
persistently mapped host-visible staging buffer once at frame size; every
frame the host memcpys intomappedand passesbufferas
InputFrame::pixelBuffer, and the chain records the buffer→image copy at
the top of its own frame submit (shader-read → transfer-dst → copy →
shader-read barriers, before history blits and passes run). Per frame:
one command buffer, one submit, one fence wait, zero staging churn —
motivated by the DevilutionX integration, where the extra submit+drain
cycle dominated the frame budget with cheap presets (a passthrough preset
cost ~4.4 ms/frame over the no-shader path).Image::updatestays for
headless/test use. - shader_demo
r/reload— re-reads the current preset from disk and
rebuilds its chain: the edit-slangp → reload → see-result loop without
restarting the demo. The fresh parse goes into a side slot and the cache
entry swaps only after the rebuilt chain replaced the old one, so the
cache-outlives-chain invariant holds at every step and a failed re-parse
leaves the previous preset + chain fully intact. Runtime parameter
overrides are cleared by a reload (the chain is rebuilt from file
values). First entry of the M4.04-post follow-up wave. - Process-global glslang frontend mutex (freeze-review F-1, decision
D-3a):docs/API.md§5.3/§6 claimed "glslang is process-globally
guarded", butGlslangRuntimeguarded lifecycle only —
InitializeProcess()once,FinalizeProcess()at exit, no lock ever
taken (the<mutex>include was dead weight). Two frontend sites ran
unserialized —CompileStageToSpv'sTShader::parse/link/
GlslangToSpvand the translate-time dead-code oracle's
TShader::preprocess— so two concurrentPreset::loads could race
inside glslang, with nothing in CI exercising concurrency. One
process-globalstd::mutex(GlslangFrontendMutex, defined in
slang_compile.cpp, forward-declared inslang_translate.cppper the
EnsureGlslangRuntimepattern) now serializes both entry points;
seconds-scale loads make contention irrelevant and the written threading
claim is true as published. API.md §4.4/§5.3 state the contract.
Changed
Chain::resizedrops its two dead input-size parameters
(freeze-review F-2, decision D-1a; pre-freeze signature break):
resize(inputW, inputH, outputW, outputH)is now
resize(outputWidth, outputHeight). The review proved all three
documented input-size clauses wrong — pass FBOs derive from the
per-frameInputFramesize plus the stored target at render, and pass
targets, the history ring and the mip copy are (re)created lazily on
size drift; the impl only validated the output size and stored it (the
input params were even commented out of the definition). Call sites were
ours (test_runner_m2.cpp,shader_demo.cpp). API.md §5.4 rewritten to
the lazy-rederive semantics; §8 gains the host duty that a swapchain
extent change (window resize /VK_SUBOPTIMAL_KHR) must be followed by
resize()with the new extent before the next render (F-3 — render
rejects a drifted extent withinvalid_argument).docs/API.mdfolds the eight freeze-review findings (Task 29):
§3 names the library's only two stderr writers as documented exceptions
and documentsSLANGVK_FILL_DEBUG(F-8, decision D-4a); §5.4 states
setParameteris write-only by design — no getter in v1, UI-facing
hosts track the merged view fromparameterInfo()+ their own calls
(F-2's getter gap, decision D-2b); §4.2 documentsResolvePresetFilePath
refusing out-of-root hits as the deliberate SEC-01 sandbox behavior
(F-5); §4.3 enumerates the 14UniformTypevalues (F-7); §5.5 carries
the header default ofSwapchainFrame::format(F-6) and the three
undocumented render-time validations — offscreen target needs a concrete
ChainOptions::outputFormat,SwapchainFrame::formatUNDEFINED/
zero-texel rejection, nullInputFrame::imagewhen history/mip is
required (F-4); §9 cross-references the stderr exceptions.
Fixed
runner.hppbuilds without VMA on the include path. The mapped-buffer
API putVmaAllocationinto the public header, whose only external
include had beenvulkan/vulkan.h(VMA is linked PRIVATE —
vk_mem_alloc.hreached only thesrc/vk/TUs), so the first public
consumer TU (src/c_api.cpp) failed the CI build with "'VmaAllocation'
does not name a type". Fixed with aVK_DEFINE_HANDLE(VmaAllocation)
forward declaration: it expands to the identical
typedef struct VmaAllocation_T* VmaAllocationthatvk_mem_alloc.h
emits, so both include orders are legal identical-typedef redeclarations
and the public header keeps its single Vulkan-headers dependency.- shader_demo
paramsnow shows live values. It printed the preset
default as the current value, so afterset BRIGHTNESS 3.0the display
still read1.250— indistinguishable from "the set didn't take" (the
exact confusion behind the maintainer's live-parameter field report).
Runtime overrides are now tracked per session and reported with a
[runtime override]tag; the lib path itself was never broken —
verified by byte-different PNG dumps before/afterseton llvmpipe.
Help text and both CI artifact USAGE blocks documentreloadand the
file-edits-need-reload rule.
Full Changelog: M4.04...M4.05