-
Notifications
You must be signed in to change notification settings - Fork 0
Design
The 24 rules below collapse into six categories. Every behavioural choice VCK ships should resolve to one of these. New rules go under whichever category they fit; no rule lives outside the index.
| # | Category | Slogan | Rules |
|---|---|---|---|
| I | Explicitness | No magic, no hidden behavior, no global state — all choices live in cfg. |
R1, R6, R10, R23, R24 |
| II | Ownership | Core owns. Layers borrow. Strict lifecycle order. VCK destroys only what it created. | R2, R3, R5, R22 |
| III | Synchronisation | No hidden sync. Explicit fences / semaphores / timeline only. Frame is the unit of truth. External sync is the caller's responsibility. | R4, R8, R17, R18 |
| IV | Cost & Scope | Minimal core surface. No engine assumptions. Zero cost for unused features. | R15, R16, R19 |
| V | Reliability | Deterministic frame behaviour. Explicit recreation events. Fail fast, fail loud — always. | R11, R12, R14 |
| VI | Transparency | User owns the frame. Escape hatches everywhere. Debuggability is core. Every public API has an example. VCK.h is the surface. Extensions are logged. |
R7, R9, R13, R20, R21, R23 |
R23 spans I + VI: it's both an explicitness rule (extensions are not silent) and a transparency rule (the user can read the log to know what their device is running). That overlap is intentional.
-
Explicit > magic. No hidden allocations, no singletons, no implicit lifetimes. Every
Initializehas a matchingShutdownin a documented order. -
No ownership in the expansion / execution layer. Every class borrows core objects by reference or raw pointer; never creates or destroys them.
-
Strict lifecycle.
Init: Context → Device → Swapchain → Pipeline → Command → Sync → (Scheduler / VMM)
Shutdown: (Scheduler / VMM) → Sync → Command → Pipeline → Swapchain → Device → Context
Expansion objects and VMM resources must be shut down before the core objects they reference.
-
No hidden synchronisation. Only
Shutdown()paths may callvkDeviceWaitIdle; the runtime hot path never does (v0.3). The remaining blocking sites are the concrete allow-list rule 8 expands on:
-
VulkanOneTimeCommand::End—vkWaitForFenceson the per-command fence, setup paths only. Short-circuits on submit failure (no infinite hang). -
VulkanMemoryManager::SubmitStagingCmd—vkWaitForFenceson a per-submitVkFence(v0.3 replaced the oldvkQueueWaitIdle). Release / acquire ownership barriers when the transfer queue is dedicated. -
FrameScheduler::BeginFrame/EndFrame—vkWaitSemaphoreson the scheduler's timeline (v0.3) or, when the device doesn't exposeVK_KHR_timeline_semaphore, per-slotvkWaitForFences. -
FrameScheduler::DrainInFlight— the scheduler's own full drain (used by the scheduler-awareHandleLiveResizeoverload instead ofvkDeviceWaitIdle). -
BackpressureGovernor::WaitIfOverrunforAsyncMax. -
VulkanSwapchain::Recreate(w, h, drainedExternally = false)—vkDeviceWaitIdlearound swapchain/framebuffer rebuild by default; the scheduler-awareHandleLiveResize(window, sc, fb, pipe, scheduler)overload drains viaFrameScheduler::DrainInFlight()and passesdrainedExternally = trueso the global wait is skipped. Both paths log viaVCKLog::Notice("LiveResize", ...)andVCKLog::Notice("Swapchain", "Recreating ..."). - Anything you do manually.
-
Frame-scoped or persistent, nothing else. Every GPU resource has a clear lifetime tag (VMM) or is owned by a class that does.
-
NO HIDDEN BEHAVIOR
- Nothing happens “automatically” without being explicitly traceable.
- Resize, recreation, batching, sync must be visible in logs/timeline.
- USER OWNS THE FRAME (UNLESS OPT-IN SCHEDULER)
- Core mode: user controls frame loop.
- Scheduler mode: explicit opt-in only.
- Never silently take control of execution.
- EXPLICIT SYNCHRONIZATION MODEL
- Only fences / semaphores / timeline tokens define ordering.
- No implicit ordering between commands, queues, or systems.
- ESCAPE HATCH ALWAYS EXISTS
- Any abstraction must allow bypass:
- raw command buffer submission
- raw queue submit
- manual synchronization
- Nothing is “forced architecture”.
- ZERO HIDDEN STATE
- No global GPU state tracking.
- No singleton device/context managers.
- All state is passed explicitly or owned by user-visible objects.
-
Caveat:
VCKLogintentionally keeps a small amount of process-global state for debug gating (SetDebug/IsDebug) and consecutive-line dedup (last key + repeat count). This is logger convention, not GPU state — it does not track resources, device handles, or anything that would violate the spirit of the rule. It is not thread-safe in the racy-reads sense (last-writer-wins on the dedup counter); log output is advisory, not a source of truth.
- DETERMINISTIC FRAME BEHAVIOR
- Same inputs → same submission order and execution graph.
- Any nondeterminism must be explicitly opt-in (Async policies only).
- EXPLICIT RECREATION EVENTS
- Swapchain/device/resource recreation must:
- be triggered
- be logged
- be observable in DebugTimeline
- never happen silently
- DEBUGGABILITY IS CORE FEATURE
- Every abstraction must be inspectable:
- LogVk output
- DebugTimeline spans
- explicit dumps of state (queues, frames, resources)
- FAIL FAST, FAIL LOUD
- No silent fallback behaviour.
- Every failure returns an explicit
bool/ error code and logs throughVCKLog::Errorwith a tag naming the subsystem. Areturn falsewithout a matchingVCKLog::Erroris a bug. -
VK_CHECKroutes non-VK_SUCCESSresults toVCKLog::Errorregardless ofcfg.debug, so a failed Vulkan call is never silent. - Errors propagate upward through return values; the kit never
std::terminates or throws across an API boundary.
- MINIMAL CORE SURFACE
- Core stays small and stable.
- Complexity belongs in: Expansion / Execution / VMM / Tools layers.
- Core must never grow into engine logic.
- NO ENGINE ASSUMPTIONS
- VCK does not assume:
- scene graph
- asset pipeline
- ECS
- renderer architecture
- It only provides execution + GPU control primitives.
- FRAME IS THE UNIT OF TRUTH
- All GPU work is bound to:
- frame slot
- or explicit timeline value
- Nothing exists “outside frame context” unless explicitly persistent.
- EXTERNAL SYNCHRONISATION
- Every VCK instance is externally synchronised, matching the Vulkan spec's thread-safety rules.
- Concurrent access to the same
VulkanDevice,VulkanSwapchain,VulkanQueue,VulkanCommand,VulkanSync,FrameScheduler, orVulkanMemoryManagerfrom multiple threads is undefined behaviour unless the caller provides the lock. - VCK never holds an internal mutex across a user-visible call. Queue submission is the caller's serialisation point.
-
JobGraphis the one exception: its worker pool is internal and owns its own synchronisation; jobs queued viaAddJobare scheduled across VCK-owned threads.
- ZERO COST FOR UNUSED FEATURES
- A module that has not been
Initialized must allocate nothing, spawn no thread, emit no log line, and hold no OS handle. - Paying for a feature requires opting in to it by name
(
FrameScheduler,VulkanMemoryManager,TimelineSemaphore,DebugTimeline,cfg.enableTimeline, …). -
VulkanContext+VulkanDevice+VulkanSwapchain+VulkanSync-
VulkanCommandis the minimum viable frame loop. Anything the user does not touch stays inert.
-
- EVERY PUBLIC API HAS AN EXAMPLE
- Every public class in
VCK.his exercised by at least one example underexample/. - New public classes (or new semantically-meaningful overloads) land in the same PR as an example that uses them.
- Examples double as executable documentation:
example/<Name>/App.cppheader block states what is demonstrated, why you'd use it, and what to look for in the console.
VCK.hIS THE API SURFACE
-
VCK.hat the repo root is the only stable include path and the only place documentation lives for the public surface. - Layer headers under
layers/{core,expansion,execution,vmm}/are implementation detail. Their paths and contents may change in any release; user code must not include them directly. - Breaking changes to
VCK.hbump the minor version (0.x) until v1.0.0. Additive changes (new class, new enum value, new overload) are patch or minor at the maintainer's discretion.
- VCK NEVER OWNS USER HANDLES
- Rule 9 guarantees a raw escape hatch. This rule completes it: VCK destroys only the Vulkan handles it created.
- Any
VkBuffer,VkImage,VkCommandBuffer,VkSemaphore,VkFence,VkPipeline, or other handle passed in to a VCK function is caller-owned. Passing it confers use for the duration of the call, never ownership or a destroy obligation. - Symmetrically, handles returned by VCK getters
(
GetSwapchain(),GetRenderPass(),GetCommandBuffer(i), …) are borrows: the user must not destroy them; they die with their VCK owner during the shutdown chain (rule 3).
- EXTENSION TRANSPARENCY
- Any instance- or device-level extension VCK enables on the user's
behalf must be announced via
VCKLog::Notice("<Subsystem>", ...)at initialisation time, including:- the extension name (
VK_KHR_timeline_semaphore,VK_EXT_debug_utils, …), - whether the adapter / loader actually supports it, and
- the fallback path taken when it does not (e.g. "timeline
semaphores unavailable —
FrameSchedulerwill use per-slot fences").
- the extension name (
- The user must never be surprised by what's running underneath. Every enabled extension is greppable in the log; every absent extension is greppable in the log; the choice between them is greppable in the log.
- Concretely:
VulkanContext::InitializeandVulkanDevice::Initializeare the two enablement points. Both must emit oneNoticeline per extension they request.
cfgIS THE CONTRACT
Anything that needs changing the code is governed by R24. The litmus test for where it lives is two lines:
-
If it changes how the user writes their renderer →
cfg. The user must be able to flip the behaviour without recompiling VCK and without re-reading the source. - If it changes how VCK works underneath → silent bundle. Pure implementation detail. No knob, no log line, no doc entry — it would be noise to the user.
Concretely:
- Every behavioural difference VCK can express that the user can
reasonably want to choose between must be reachable through
VCK::Configand its sub-structs (cfg.device,cfg.swapchain,cfg.pipeline,cfg.scheduler,cfg.aa,cfg.debug, …). Nothing in this set is hardcoded silently inside a.cpp. - Defaults belong on the field declaration in the struct, not in
code. A user who never touches
cfggets the default behaviour; a user who reads the struct sees every knob the kit exposes in one place. - Adding a runtime branch on something other than a
cfgfield (compile-time#define, environment variable,extern bool, hidden static) for a user-visible behaviour violates this rule. The single existing exception isVULKAN_VALIDATION— a build-config toggle, not a runtime choice, documented inVulkanContext.cpp. -
Silent bundles are explicitly allowed and expected. If VCK
changes how something works internally (e.g. switching from
vkQueueWaitIdleto a per-submit fence, or reordering pool creation, or batching N submits into one) without changing the user-facing API or the rendered output, the change ships as part of the version that introduces it and does not get acfgknob. R23 still applies — extension enablement is never silent — but the plumbing is. Asking "does the user write a different line of renderer code if I flip this?" decides which side of the line it falls on.
-
Dedicated queues (v0.3).
VulkanDevice::FindQueueFamiliespicks a compute-only and a transfer-only family when the vendor exposes them. Fallbacks to the graphics family are logged viaVCKLog::Notice("Device", ...). Thread safety is governed by rule 18 — differentVkQueues are independent external-sync scopes, so graphics + compute + transfer submits from separate threads are safe. -
Timeline semaphores (v0.3).
VulkanDevice::InitializeenablesVK_KHR_timeline_semaphorewhen the adapter supports it (Vulkan 1.2+).VulkanDevice::HasTimelineSemaphores()exposes the capability.FrameSchedulerautomatically uses a single per-scheduler timeline for frame retirement; per-slot fence path remains as a fallback. Driven by rule 19: the timeline is only allocated when bothcfg.enableTimelineandHasTimelineSemaphores()are true. -
VMM staging (v0.3).
VulkanMemoryManager::SubmitStagingCmdno longer callsvkQueueWaitIdle; it uses a per-submitVkFence. When the transfer queue is dedicated, staging runs on the transfer family and a release/acquire ownership-barrier pair is recorded so the graphics queue sees the expected image layout (Vulkan §7.7.4). CPU-serialised for v0.3; a semaphore-driven async acquire is on the v0.4 roadmap. -
Secondary command buffers (v0.3).
VulkanCommand::AllocateSecondary / BeginSecondary / EndSecondary / ExecuteSecondariesare the record-in-a-secondary / execute-via-vkCmdExecuteCommandspath. The pool is shared with the primaries; multi-threaded allocation is the caller's sync responsibility (rule 18). -
LogVkmigration (v0.3). Every call site in core / expansion / execution / vmm / example routes throughVCKLog::{Info, Notice, Warn, Error}with a subsystem tag (rule 14). The oldLogVkfree function remains as a shim so user code written against v0.2 still compiles. -
Extension matrix (rules 23 + 24, post-v0.3).
VulkanDevice:: CreateLogicalDeviceenumerates device extensions once and runs two passes:-
Silent bundle (R24 silent path). When the device advertises
them, VCK enables
VK_KHR_synchronization2,VK_KHR_buffer_device_ address,VK_EXT_memory_budget,VK_EXT_device_fault,VK_KHR_present_wait,VK_KHR_present_id. No public API surface; symbols are reachable for v0.4 use sites (sync2 inFrameScheduler, BDA in VMM,memory_budgetpolling inDebugTimeline,present_wait/present_idpacing inFrameScheduler). Each result is oneVCKLog::Notice("Device", "ext enabled (bundle): ...")/"ext unavailable (bundle): ..."line — R23. -
cfg-gated (R24 user-visible path).cfg.rendering.mode = RenderingMode::DynamicrequestsVK_KHR_dynamic_rendering,cfg.device.enableBindlessrequestsVK_EXT_descriptor_indexing,cfg.swapchain.presentMode = PresentMode::FifoLatestReadyrequestsVK_EXT_present_mode_fifo_latest_ready. Stage-1 surface: the extension is enabled and announced, but the rendering / bindless codepaths themselves ship in v0.4; today the request is acknowledged with a fallbackNoticeand behaviour stays Classic / non-bindless. The present-mode knob is fully wired — when the extension is present,VulkanSwapchain::ChoosePresentModereturnsVK_PRESENT_MODE_FIFO_LATEST_READY_EXTdirectly.
-
Silent bundle (R24 silent path). When the device advertises
them, VCK enables
-
JobGraphis a correct-but-simplestd::thread+ condvar scheduler. No fibres, no work-stealing. Drop-in replacement planned when a real workload demands it. -
DebugTimelinedumps as plain text toVCKLogand optionally exportschrome://tracingJSON viaDumpChromeTracing(path)(v0.2.1). No in-repo graphical viewer — usechrome://tracingorhttps://ui.perfetto.dev. - Supported platforms: Windows (MinGW-w64 g++), Linux, macOS
(latter two via
VCK::Window+example/build.sh, pkg-configvulkan/glfw3). MSVC/cl is not wired today — mechanical port.
-
MSAA end-to-end.
cfg.swapchain.msaaSamples > 1works end-to-end:VulkanSwapchainowns the per-image multisampledVkImage+view,VulkanPipelineconfigures the render pass with a resolve attachment,VulkanFramebufferSetbinds[msaaView, swapchainView], recreate on resize rebuilds both. Zero-config unchanged.MSAA_AUTOsentinel +DetectRecommendedMSAApick a sensible sample count on firstInitialize(integrated → 1x, mid discrete → 4x, high discrete → 8x, clamped toframebufferColorSampleCounts∩ depth counts). -
AA auto-detection (first
Initialize).cfg.aa.technique = AATechnique::AutorunsDetectRecommendedAA(device, forwardRenderer, motionVectors)— 5-step decision tree (VRAM tier → renderer path → motion-vector support) picks MSAA / MSAA_A2C / SampleRate / FXAA / SMAA_1x / SMAA_T2x / TAA / TAAU. Sample-based techniques are wired by VCK end-to-end; post-process techniques are name-only and the renderer implements the shader (rule 15/16 —VCKis not a renderer).VulkanSwapchain::GetAATechnique()returns the resolved pick. -
Logger polish.
VCKLoggained an explicitLevelenum (Infodebug-gated,Notice/Warn/Erroralways visible), a consecutive-line dedup that prints(repeated N more times)on the next distinct line, and aSetDebug(bool)knob tied tocfg.debug. LegacyLogVk("[Tag] msg")sites auto-parse the[Tag]prefix intoVCKLog::Info(tag, body)so no mass call-site churn was needed. -
Cross-platform facade.
VCK::Window+VCKCrossplatform.{h,cpp}cover Windows/Linux/macOS; all 9 examples use it, no raw GLFW/HWND in user code.example/build.shmirrorsbuild.baton Linux/macOS. -
Live resize as a first-class feature.
VCK::HandleLiveResize()one-call-per-frame, auto-tracks size, logs[LiveResize]spans.
VCK splits AA into two families and implements exactly one of them:
| Family | Techniques | Who implements? |
|---|---|---|
| Sample-based |
MSAA, MSAA_A2C, SampleRate
|
VCK |
| Post-process |
FXAA, SMAA_1x, SMAA_T2x, TAA, TAAU
|
Caller |
Sample-based techniques map directly to VkPipelineMultisampleStateCreateInfo
fields (rasterizationSamples, alphaToCoverageEnable,
sampleShadingEnable + minSampleShading). Post-process techniques are
render-pass features that belong to a renderer — shipping them would grow
the core beyond rules 15/16. VCK picks the technique name, exposes it
via VulkanSwapchain::GetAATechnique(), and the application's own
post-process pass implements the shader.
Vendor-specific techniques (CSAA/EQAA/TXAA/MFAA) are intentionally not offered — they require vendor extensions that break the cross-platform promise. Any app that needs one should pin the samples and enable the extension directly; VCK's escape hatch (rule 9) is right there.
DetectRecommendedAA(VkPhysicalDevice, forwardRenderer, motionVectors):
-
Query hardware —
VkPhysicalDeviceProperties+VkPhysicalDeviceMemoryProperties(device-local heap sum). - Classify tier — LOW (integrated OR ≤ 2 GB), MID (≤ 6 GB), HIGH (> 6 GB).
-
Detect renderer path — caller-supplied (
forwardRenderer); deferred pipelines can't use MSAA efficiently. -
Select method:
- LOW →
FXAA - MID + forward →
MSAA_A2C - MID + deferred →
SMAA_T2x(if motion vectors) elseSMAA_1x - HIGH + forward →
TAA(if motion vectors) elseMSAA_A2C - HIGH + deferred →
TAA(if motion vectors) elseSMAA_1x
- LOW →
-
Clamp MSAA samples —
DetectRecommendedMSAApicks an actual sample count (1/2/4/8) clamped to bothframebufferColorSampleCountsANDframebufferDepthSampleCounts.
Every decision logs a VCKLog::Notice("AA", ...) line so the user sees
what got picked without enabling debug.
Shipped in v0.3:
- Enable
timelineSemaphoreonVulkanDeviceand wireFrameSchedulerto use timeline primitives throughout. - Real dedicated transfer / compute queues in
VulkanDeviceandQueueSet. - Async staging path in VMM — fence-per-submit, no
vkQueueWaitIdle. Release/acquire ownership barriers across queue families. - Secondary command buffer support in
VulkanCommand. - Remove runtime
vkDeviceWaitIdle— scheduler-awareHandleLiveResizedrains viaFrameScheduler::DrainInFlight(). -
chrome://tracingexport (v0.2.1:DebugTimeline::DumpChromeTracing).
Deferred, in rough priority order:
- Semaphore-driven async acquire in VMM staging (today's v0.3 path CPU-serialises the acquire after the transfer fence retires).
- GPU-driven indirect-draw sample (compute generates
vkCmdDrawIndirectcommands). - Bundled graphical profiler viewer (Perfetto / chrome://tracing is the external viewer today).
- MSVC/cl toolchain support (currently MinGW-w64 g++ only on Windows).
- Unit-test harness (currently only CI-gated compile + manual Windows validation).
- Home
- Quick Start
- Your First App
- Understanding cfg
- Build — Windows / Linux / macOS
Single source of truth for the full API surface is the doc block at the top of VCK.h.