Skip to content

M4.05

Pre-release
Pre-release

Choose a tag to compare

@VTSTech VTSTech released this 20 Oct 21:34
· 42 commits to main since this 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.h only, extern "C", consumable from any FFI language)
    mirroring the C++ API 1:1. slangvk_result mirrors slv::Errc
    code-for-code, slangvk_result_name() names codes for logs and
    thread-local slangvk_last_error() carries the last failed call's
    message; opaque slangvk_device / slangvk_image / slangvk_preset /
    slangvk_chain handles; 25 entry points covering device create
    (library-owned) or adopt (host-owned instance/device/queue — the
    DevilutionX mode) plus get_handles/get_info/wait_idle/
    read_pixels, image create/update/get_handles, preset load +
    pass/parameter queries, and chain create/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.hpp pulls the header in; both slangvk and slangvk-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 standalone Image::update round-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 into mapped and passes buffer as
    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::update stays 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", but GlslangRuntime guarded lifecycle only —
    InitializeProcess() once, FinalizeProcess() at exit, no lock ever
    taken (the <mutex> include was dead weight). Two frontend sites ran
    unserialized — CompileStageToSpv's TShader::parse/link/
    GlslangToSpv and the translate-time dead-code oracle's
    TShader::preprocess — so two concurrent Preset::loads could race
    inside glslang, with nothing in CI exercising concurrency. One
    process-global std::mutex (GlslangFrontendMutex, defined in
    slang_compile.cpp, forward-declared in slang_translate.cpp per the
    EnsureGlslangRuntime pattern) 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::resize drops 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-frame InputFrame size 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 with invalid_argument).
  • docs/API.md folds the eight freeze-review findings (Task 29):
    §3 names the library's only two stderr writers as documented exceptions
    and documents SLANGVK_FILL_DEBUG (F-8, decision D-4a); §5.4 states
    setParameter is write-only by design — no getter in v1, UI-facing
    hosts track the merged view from parameterInfo() + their own calls
    (F-2's getter gap, decision D-2b); §4.2 documents ResolvePresetFilePath
    refusing out-of-root hits as the deliberate SEC-01 sandbox behavior
    (F-5); §4.3 enumerates the 14 UniformType values (F-7); §5.5 carries
    the header default of SwapchainFrame::format (F-6) and the three
    undocumented render-time validations — offscreen target needs a concrete
    ChainOptions::outputFormat, SwapchainFrame::format UNDEFINED/
    zero-texel rejection, null InputFrame::image when history/mip is
    required (F-4); §9 cross-references the stderr exceptions.

Fixed

  • runner.hpp builds without VMA on the include path. The mapped-buffer
    API put VmaAllocation into the public header, whose only external
    include had been vulkan/vulkan.h (VMA is linked PRIVATE —
    vk_mem_alloc.h reached only the src/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 a VK_DEFINE_HANDLE(VmaAllocation)
    forward declaration: it expands to the identical
    typedef struct VmaAllocation_T* VmaAllocation that vk_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 params now shows live values. It printed the preset
    default as the current value, so after set BRIGHTNESS 3.0 the display
    still read 1.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/after set on llvmpipe.
    Help text and both CI artifact USAGE blocks document reload and the
    file-edits-need-reload rule.

Full Changelog: M4.04...M4.05