Theme Catppuccin
Make yourself at homeCatppuccin

A softer light for long learning sessions.

Appearance

Loading your preference…

Dark palette

Choose a dark palette to try it.

Light uses Latte. Your choices stay in this browser.

Back to your source shelf

Inside the companion

Renderer Engineering

Explore bounded image work, allocator lifetimes, cancellation, and Asio coroutines.

Project files 15

Build the Living Gallery experiments

Run the bounded image service, PMR comparison, and optional Asio coroutine study.

284 lines README.md
# 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<Job>, 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<Outcome>` 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).

This is the file included with the course. Copy and download give you its complete source.

Taking your work offline? Download all course companions