# The Living Gallery · C++23 engineering studio Build a bounded background image service, investigate real renderer scratch storage, and suspend a coroutine using an established execution library. Each program finishes a small task; the examples do not constitute a general engine. Use C++23, CMake 3.25+, Ninja, and Clang or GCC with a standard library that implements the used C++23 facilities, including `std::expected`. No C++26 or reflection is needed. The verified compiler/platform is listed below. All command blocks below start at the **Parallax project root**. Build directories and generated images are local; publishing the website does not run C++ in a browser. Executables retain the historical `prism-` prefix to keep commands consistent with the course companions. ## 1. Build and save a real image ```sh cmake -S native/advanced/engineering -B build-gallery -G Ninja \ -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Debug cmake --build build-gallery ctest --test-dir build-gallery --output-on-failure ./build-gallery/prism-gallery build-gallery/gallery.ppm ./build-gallery/prism-allocators build-gallery/tiles.ppm ``` For GCC, choose `-DCMAKE_CXX_COMPILER=g++` in a **new** build directory. The default configuration has six CTest targets. Open the resulting PPM files in an image viewer; the first is a 32×32 gradient, the second a 64×64 triangle coverage study. The tests check known pixels independently of the allocator comparison. Start with these source checkpoints in order: | Checkpoint | Read / edit | Observable result | | --- | --- | --- | | Understand the blocking baseline | `src/worker_pool.cpp`, `src/experiment.cpp` | A checksum batch joins before returning | | Admit an image without waiting for rendering | `include/prism_engineering/asset_pipeline.hpp`, `src/asset_pipeline.cpp` | Eight occupied slots reject a ninth request | | Keep the current image | `src/gallery_main.cpp`, `tests/asset_pipeline_tests.cpp` | B publishes before A; A is rejected as stale; C fails and B survives | | Render scratch tiles | `src/allocator_lab.cpp` | One temporary accumulation tile at a time, independent output pixels | | Check allocator operations | `tests/allocator_tests.cpp` | Copy, move, equality, assignment, exhaustion, and borrower lifetimes | | Suspend using a runtime | `src/coroutine_main.cpp` | Success, cancellation, and exception delivery; payload cleanup | Each course checkpoint has an exercise and three folded hints. A source excerpt is labeled as an excerpt; full files are available through the source viewer. ## 2. Responsive CPU work and publication `run_batch(vector, Options, stop_token)` remains a **blocking reference**: 32 jobs maximum, 4096 samples per job, one to eight workers, joined completion. Constructing an argument may allocate before entry validation, and moved vectors may retain capacity beyond their size. The reference bounds accepted work rather than every byte a caller can own. Its `accepts` helper demonstrates publication policy on values. `AssetPipeline` is the next step: it owns one to four `jthread`s at a stable address and accepts owned image descriptors by value. Dimensions must each be 2–64, with at most 4096 logical pixels per image. Its eight fixed slots cover **queued + running + completed-but-unconsumed** requests. Taking a terminal result moves its owning image out and frees that slot. The caller can retain arbitrary results after taking them, so this is a service retention bound, not a process memory cap. Allocator metadata, capacity, and the caller's published image are outside the slot-count contract. `try_submit` and `try_take` acquire a short mutex and can contend. They do not wait for rendering or queue capacity; neither is claimed to be lock-free or wait-free. A real event loop calls `try_take` between its other activities. `wait_take` is a blocking CLI/test convenience; do not use it on a UI event thread that must remain responsive. While admission is open, wait_take waits even when no job exists. After request_stop, call shutdown before draining: only shutdown finalizes queued slots; request_stop alone does not promise a terminal result for them. This is background **CPU image preparation**, not asynchronous I/O. One application thread owns all public pipeline/Gallery calls; destruction and shutdown must not race those calls. Workers access private state under the mutex, then compute outside the lock. The service owns the descriptor, each worker owns its in-progress vector, and each completion owns its finished image. No borrowed pixel input survives a caller scope. Optional shared gates are deterministic test seams and remain alive through worker use. The publication trace is checked with gates, not sleeps: ```text A(1) accepted -> worker arrives at closed gate B(2) accepted -> second worker finishes -> publish B A gate opens -> A finishes -> reject stale A C(3) accepted -> injected failure -> record failure, keep B D(4) accepted -> stop and join -> record cancellation, keep B ``` `Gallery::request` advances requested identity only after admission succeeds. `Gallery::apply` rejects stale, pending, or duplicate completion before mutation. Current failure/cancellation updates status but retains the previous owning image. Generations must increase strictly; zero, reuse, and wrap are rejected. No unchecked increment is performed at the maximum identifier. This example represents one asset; a multi-asset system needs an explicit identity and retention policy. Shutdown closes admission, requests cooperative stop, joins all started workers, and finalizes any remaining admitted slots. Workers observe stop at their gate and between rows. A completed success is not retroactively changed into a cancellation. Stop cannot interrupt arbitrary I/O or undo side effects. Explicit `shutdown` makes the boundary visible; the destructor applies it as a fallback. Worker exceptions are contained, and failed partial startup stops/joins workers already created. `runtime_failed()` reports exceptional worker-loop failures outside ordinary image preparation; callers should shut down and inspect that condition rather than assume every missing result will eventually arrive. The tests cover partial startup, invalid dimensions, exact slot pressure, retained completed results, cancellation at a closed gate, known image corners, B-before-A completion, stale failure, duplicates, preserved image after failure, and maximum generation. `expected` communicates selected errors, not a blanket no-throw guarantee; the executable boundary also catches exceptions. ## 3. The allocator studio `render_plain` first establishes an ordinary `std::vector` baseline. `render_tiles` uses the same 64×64 algorithm, but each 16×16 accumulation tile borrows a selected scratch resource. Four integer samples classify a diagonal triangle/background; the edge pixel (32,31) must average to `(186,60,64)`. This is an integer coverage and allocation study, not a physically based or display-color pipeline. Resolved pixels are copied into an **ordinary owning output vector** before the temporary tile dies. The output can survive every scratch resource. No view or PMR buffer is returned as if moving it removed its resource dependency. ```text output vector: allocate ---------------- resolve tiles ---------- return owner scratch tile: construct -> use -> destroy (16 times) resource: construct -------------------------------- release -> destroy ``` The ordinary resource policy forwards requests to `new_delete_resource`; the monotonic policy retains backing storage until release; the pool policy groups blocks by size and can reuse them. We use an unsynchronized pool only within a single-threaded measurement. Pool chunk behavior and early upstream deallocation are implementation-dependent; do not demand an exact allocation count or speedup. Two noncopyable counting wrappers distinguish logical container requests from actual upstream traffic. Resource equality is identity-based because returning one counter's allocation through a different counter would corrupt accounting. Same upstream heap does not imply interchangeable resource instances. Run an optimized measurement separately: ```sh cmake -S native/advanced/engineering -B build-gallery-release -G Ninja \ -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Release cmake --build build-gallery-release ./build-gallery-release/prism-allocators > allocator-results.csv ``` The CSV includes ten samples per policy, with run 0 retained and no discarded warmup. `request_calls`, `requested_bytes`, and `peak_requested` describe the tile containers. `upstream_calls`, `upstream_requested`, `upstream_peak`, `retained_upstream`, and `after_release` describe the policy's backing traffic. These counters omit output storage and the ordinary allocator's private metadata; none measures process resident memory. Exact observed sizes are not portable contracts. `after_release` must be zero at the upstream wrapper. `elapsed_ns` times output allocation, tile rendering, scratch allocation and container destruction, with instrumentation overhead. It excludes resource construction/release, final image destruction, printing and file I/O. Upstream counts can include resource construction even though that time is excluded. This is an exploratory workload comparison; ten tiny renders do not establish a universal fastest allocator. Keep the pixels, workload, build mode, compiler and machine fixed, inspect the raw rows, and report a median plus limitations. The allocator tests distinguish these operations: | Operation on PMR vectors | Destination resource | | --- | --- | | Ordinary copy construction | Current default resource | | Copy with explicit allocator | Supplied resource | | Ordinary move construction | Source's allocator | | Move with unequal explicit allocator | Supplied resource; element transfer can allocate | | Copy or move assignment | Existing destination allocator | | Swap | Used only when allocators compare equal | Do not require a moved-from container to be empty. Do not perform unequal non-propagating allocator swaps. A resource supplies storage and does not destroy live objects when `release()` is called: end borrower scopes first. Tests use an aligned 64-byte buffer and `null_memory_resource` upstream to deliberately reject a large reserve and an actual render. They verify the previous value survives failure; they never dereference freed scratch storage. ## 4. A real coroutine runtime, with a bounded experiment Asio is **optional**. This target uses standalone **1.38.2**, release archive `https://github.com/chriskohlhoff/asio/archive/refs/tags/asio-1-38-2.tar.gz`, SHA256 `9f2648fa483e58a6bf848d970ee0ea650ca19ed7769dfa520ed4f7b8d27af1db`. CMake checks the hash and keeps the dependency in the build tree. Asio uses the Boost Software License 1.0; retain its license if redistributing its source. ```sh cmake -S native/advanced/engineering -B build-coroutines -G Ninja \ -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Debug -DPRISM_ASIO=ON cmake --build build-coroutines ctest --test-dir build-coroutines --output-on-failure ./build-coroutines/prism-coroutines ``` The first configure fetches source. For offline builds, provide a previously verified extracted release tree (the directory containing `asio/include`): ```sh cmake -S native/advanced/engineering -B build-coroutines-offline -G Ninja \ -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Debug -DPRISM_ASIO=ON \ -DFETCHCONTENT_SOURCE_DIR_ASIO=/absolute/path/to/asio-asio-1-38-2 cmake --build build-coroutines-offline ctest --test-dir build-coroutines-offline --output-on-failure ``` That override bypasses archive download/hash verification: you must verify the archive when obtaining it and preserve the extracted tree. It is not a mechanism for silently using a different version. There is no global dependency install. The program runs three independent scenarios with exactly one operation and one `io_context` runner at a time: ```text success: expired wait -> completion; payload released cancel: started -> other handler -> aborted wait -> completion; payload released failure: exception observed; payload released 3 bounded coroutine scenarios passed ``` `wait_owned` is a named `asio::awaitable` coroutine with owned value parameters. `co_spawn` assigns its executor and an observed completion callback. No custom promise/task runtime or detached completion is used. The weak payload probe remains alive after the caller resets its owner, then expires after the context drains and the frame cleans up. Ordinary callback references remain valid because their scope encloses `run()` and its completion checks. The cancellation case posts a normal handler before awaiting a far-future timer. `post` is not inline; with one runner, that handler cannot run until the coroutine has suspended in the pending wait. Its associated cancellation signal emits terminal cancellation. `redirect_error(use_awaitable, error)` exposes the aborted wait as an error code; other failures remain explicitly checked. The posted handler proves the executor can process another activity while this operation is pending. A timer is real asynchronous waiting, not CPU-parallel rendering or async file I/O. Graceful shutdown drains `io_context::run()` until operation and completion finish. Calling `stop()` is not used as a substitute for observing completion. The example has no work guard and starts no subsequent waits after cancellation. A real network/file operation needs its own cancellation and buffer-lifetime contract; this timer's support is not universal. The test timeout is only a watchdog, not an ordering assumption. There are no sleep-based tests. ## 5. Sanitizers and verification boundaries ```sh cmake -S native/advanced/engineering -B build-gallery-sanitize -G Ninja \ -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Debug \ -DPRISM_SANITIZERS=ON -DPRISM_ASIO=ON cmake --build build-gallery-sanitize ctest --test-dir build-gallery-sanitize --output-on-failure cmake -S native/advanced/engineering -B build-gallery-tsan -G Ninja \ -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Debug \ -DPRISM_THREAD_SANITIZER=ON -DPRISM_ASIO=ON cmake --build build-gallery-tsan ctest --test-dir build-gallery-tsan --output-on-failure ``` ASan/UBSan and ThreadSanitizer use separate build directories and cannot be enabled together. Warnings and instrumentation are private options on all owned targets; the library publishes its C++23 and thread usage requirements. Sanitizers inspect executed paths, not every schedule, potential deadlock, or correctness property. Local verification on 24 October 2026 used Apple Clang 21.0.0, libc++, CMake 4.4.4, Ninja, and macOS arm64. Debug, Release, ASan+UBSan, and a separate TSan build with Asio enabled each passed seven CTest targets without sanitizer diagnostics. Tests remain active under `NDEBUG`. See `audits/async-allocator-update.md` at the project root for checks and negative controls. Linux/GCC runtime execution was not available in this run; portable C++ design is not a claim of that platform test. No GPU resource, decoder, cache, or graphical event loop is created here. ## Primary contracts and review reading - [C++23 working draft N4950](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/n4950.pdf): version boundary; living-draft links below identify the relevant library clauses. - [Allocator-aware containers](https://eel.is/c++draft/container.alloc.reqmts), [polymorphic allocator](https://eel.is/c++draft/mem.poly.allocator.class), [memory_resource equality](https://eel.is/c++draft/mem.res.eq). - [Monotonic resource](https://eel.is/c++draft/mem.res.monotonic.buffer) and [pool resources](https://eel.is/c++draft/mem.res.pool). - [jthread lifetime](https://eel.is/c++draft/thread.jthread.class) and [stop-token condition-variable waits](https://eel.is/c++draft/thread.condvarany). - [Asio 1.38.2 coroutine support](https://think-async.com/Asio/asio-1.38.2/doc/asio/overview/composition/cpp20_coroutines.html), [timer async_wait](https://think-async.com/Asio/asio-1.38.2/doc/asio/reference/basic_waitable_timer/async_wait.html), [io_context::run](https://think-async.com/Asio/asio-1.38.2/doc/asio/reference/io_context/run.html), and [post](https://think-async.com/Asio/asio-1.38.2/doc/asio/reference/post.html). - Downloaded C++ Core Guidelines reviewed: [R.5](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rr-scoped), [R.20](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rr-owner), [CP.42](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rconc-wait), [CP.51](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rcoro-capture), [CP.52](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rcoro-locks), [CP.53](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rcoro-reference-parameters).