Inside the companion
The Shader Workshop
Open a native OpenGL window, draw a triangle, then move a camera around three exhibits.
Project files 8
Bring the observatory to life
Dependencies, build commands, camera checkpoints, and capture and failure experiments.
# Chromatic Observatory — Course 2 native starter
An original C++23 / OpenGL 3.3 core / GLSL 330 starter for **The Shader Workshop**.
It creates a GLFW context, loads functions with GLEW, owns GL names with move-only
objects, validates shader compile/link results and a required uniform, uploads an
interleaved VAO/VBO, and draws a rotating colored triangle. The `--camera` checkpoint connects tested
model/view/projection matrices to three overlapping exhibits, depth testing, orbit
controls and real framebuffer resize. Hidden smoke modes capture actual P6 PPMs.
This is the beginning of the sixteen-week project. Indexed panels, a complete room,
texture upload, the characterized linear/sRGB path, materials, offscreen passes,
transparent sorting, shadow mapping, instancing, and interactive shader reload are
progressive learner extensions taught in the course. They are not implemented by
this starter. The CPU contract helpers include normalization, matrix multiplication,
Z rotation, translation, look-at views, homogeneous point/direction transforms,
a supported perspective projection, and bounded capture sizes.
## Current host-language baseline
The companions build as **C++23** with extensions disabled. The recipes select
Ninja and Clang explicitly; on macOS, `clang++` normally selects Apple Clang.
Use a fresh build directory when changing generator or compiler. CMake describes
and configures the targets, Ninja runs their build steps, and Clang compiles them.
No setup command here installs a dependency. The shader language is configured
separately from C++.
No C++26 feature is required by the GPU programs. Check the exact compiler and
standard-library support before adopting a later feature. Older idioms remain
useful when their contracts are clear: the purpose of scoped owners and checked return values here
is to make cleanup and failure paths explicit, not to replace every line with new
syntax. The independent C++23 audit is recorded in `audits/gpu-courses.md` at the
project root.
## Local dependencies
- A C++23 compiler and standard library with `std::expected`.
- CMake 3.25 or newer.
- An existing GLFW installation, version 3.3 or newer, with its CMake package.
- An existing GLEW installation and OpenGL development libraries/driver.
CMake discovers these dependencies locally. It does not download packages, install
system software, or modify the parent CPU renderer project. If discovery fails,
point `CMAKE_PREFIX_PATH` at an existing dependency prefix or configure your target
machine's development environment deliberately. Keep compiler, dependency, and
driver evidence with the project rather than assuming a setup command is universal.
AMD on CachyOS is the course's primary target. The starter does not assume a Mesa
version, session backend, or driver capability based on the GPU brand; record the
actual renderer/version on that machine. OpenGL 3.3 core is the required baseline.
OpenGL 4.3 compute and core debug callbacks are not required.
macOS is a compatibility path. Apple deprecated OpenGL in macOS 10.14; its native
implementation supports at most OpenGL 4.1. The starter requests a forward-compatible
core context on Apple. Its local verification below is not AMD/Linux verification.
## Build and run
From this directory:
```sh
cmake -S . -G Ninja -DCMAKE_CXX_COMPILER=clang++ -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build --parallel
ctest --test-dir build --output-on-failure
./build/observatory
```
Escape or the window close button ends the interactive application.
```sh
./build/observatory --smoke
./build/observatory --capture observatory.ppm
./build/observatory --bad-shader
./build/observatory --check-stage fragment path/to/study.frag
./build/observatory --check-program path/to/study.vert path/to/study.frag
```
`--smoke` creates a hidden window, draws one frame, and exits. It still requires a
working desktop display/context service; “hidden” does not mean display-independent.
`--capture` also uses one hidden frame and writes the specified path, replacing an
existing file. It reads the back buffer before presentation, validates the capture
extent/byte budget, uses tightly packed RGB rows, and reverses the row order for PPM.
It checks write and close status. Image viewers differ in their PPM support.
`--bad-shader` deliberately returns failure with a GLSL compiler diagnostic. It is
an error-path demonstration, not a passing visual test. Initial source files are
bounded to 64 KiB, and compiler logs are bounded. The application catches ordinary
exceptions at its outer boundary and reports failure on standard error.
The shader-check commands create a hidden context too. `--check-stage` compiles a
single vertex or fragment stage. `--check-program` compiles and links a supplied
pair. They do not validate application-required uniforms, update values, draw the
extension, or establish that its visual algorithm is correct. A complete vertex
stage can require a different compatible fragment stage from the starter's.
Development shader paths are compiled from this source directory so that launching
from a different working directory works. A relocated binary still needs that
source asset path; a packaged application would need a deliberate asset-location
and installation policy.
## Ordered camera checkpoints
Use the chapter-1 repository-root build, or the equivalent commands above from
this directory. The following commands assume the local `build` directory.
1. **Predict one point.** Open `contracts.hpp` and `contracts_test.cpp`. Run
`./build/observatory-contracts`: 25 CPU checks, no context. Eye `(0,0,4)` maps
world origin to `(0,0,-4,1)` and its own position to `(0,0,0,1)`. Directions
with homogeneous w=0 do not translate. The tests also cover a side view,
a known frustum edge, projection endpoints and invalid/degenerate inputs.
2. **Draw the tested chain.** Open `main.cpp` and `shaders/camera.vert`.
Run `./build/observatory --camera --capture camera.ppm`. Expect three colored
triangles at distinct translations/depths on a dark background. The shader
evaluates projection × view × model × position; the CPU uploads column-major
storage with transpose=false. All three active uniforms are required.
3. **Make depth visible.** Run
`./build/observatory --camera --reverse-draw --capture reversed.ppm`, then
`cmp camera.ppm reversed.ppm`. On the same local host the opaque noncoplanar
scene has identical bytes. If order changes visibility, inspect the depth
attachment, test enable, clear and write state before altering camera math.
This is not a transparent-order or cross-driver hash guarantee.
4. **Resize for real.** Run
`./build/observatory --resize-smoke --capture resized.ppm`. It draws once,
requests 640×480 logical size, waits up to two seconds for a changed pixel
extent, rebuilds projection and viewport, checks four-to-three aspect and
captures the second draw. The local scaled display reported 1760×1200 then
1280×960; your pixel dimensions can differ.
5. **Find home.** Run `./build/observatory --camera`. Arrow keys orbit, W/S zoom,
R resets yaw=0/pitch=0/distance=4, and Escape closes. Input is inactive while
unfocused, pitch is clamped to ±1.2 radians, distance to [1.5,10], and frame
delta to 0.05 seconds. A zero-size drawable waits for events and resets the
motion clock. Manually check minimize/restore/focus/reset on your own desktop.
Then replace an exhibit with your own indexed panel or box while preserving
the three-exhibit scene and CPU tests.
The core depth query uses `glGetFramebufferAttachmentParameteriv` with the default
framebuffer's `GL_DEPTH` attachment and `GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE`.
A legacy `GL_DEPTH_BITS` integer query failed during actual core-context testing
and was corrected. The renderer validates the actual depth attachment, enables
`GL_LESS`, and clears color/depth before each opaque draw. No legacy fixed-function
matrix stack is used.
For supported Clang/GCC sanitizer builds from this directory:
```sh
cmake -S . -B build-sanitize -G Ninja -DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_BUILD_TYPE=Debug -DOBSERVATORY_SANITIZERS=ON
cmake --build build-sanitize --parallel
ctest --test-dir build-sanitize --output-on-failure
./build-sanitize/observatory --resize-smoke
```
Sanitizer CTest checks host mathematics; the last command also exercises the real
context path. Sanitizers do not validate every GPU access or prove memory safety.
## Ownership and limits
The stack order is GLFW session → window/context → inner GL owners. All GL owners
are destroyed before their context/window. The current context stays on the same
thread for the lifetime of these owners. Their Debug assertions check that contract;
the wrappers do not constitute a general context scheduler or GPU lifetime checker.
Object names are not copied. Moving transfers the name and leaves a zero name in
the source owner.
The initial vertex data is fixed and trusted. Any external mesh extension must
validate counts, index ranges, byte products, and GL integer conversions before
upload/draw. Capture accepts positive dimensions up to 8192 per side and at most
16,777,216 pixels. A minimized framebuffer is skipped in the interactive loop.
The first palette is artistic. It is not a characterized linear/sRGB output path;
chapter 5 teaches the learner to establish that contract. RAII does not make C++
memory-safe, prove indices correct, or establish completion of queued GPU work.
## Verification performed
Verified locally on 2026-10-24:
- AppleClang 21.0.0.21000334, C++23, Debug build; warning flags enabled; build completed
without diagnostics.
- Locally discovered GLFW 3.5.1 and GLEW 2.3.1; no dependency installation.
- `ctest`: one target passed, exercising **25 CPU contracts**, including zero/non-finite
normalization, rotation, projection bounds and near/far mapping, exact capture
budget and an over-budget row. This target creates no graphics context.
- Actual hidden-context capture returned zero and reported `GL_VERSION: 4.1 Metal - 91.7`
and `GL_RENDERER: Apple M4 Pro`. The resulting 1760 × 1200 framebuffer PPM was
converted for visual inspection and showed the expected colored triangle on the
dark background. Framebuffer size differed from the logical 880 × 600 window.
- Intentional invalid GLSL returned one with a stage compiler diagnostic. Debug
context-lifetime assertions remained satisfied during error unwinding.
- Both complete GLSL course snippets compiled in actual desktop contexts. The
chapter 3 pulse fragment also linked with the starter vertex stage; the chapter 4
position-only camera vertex stage linked with a compatible constant-output
fragment stage. These checks establish stage/program validity, not application
uniform updates or visual correctness of those learner extensions.
The first sandboxed desktop run lacked WindowServer service access. The verified
graphics runs used the tool's approved desktop access, and the blocked first process
was stopped. This distinguishes an execution-environment restriction from a driver
or shader failure.
The new camera checkpoint additionally builds and passes CPU contracts in strict
Debug, Release and ASan/UBSan configurations. Real Debug/Release camera captures
match on this host; normal/reversed submission captures match; a real resize
passes in both Debug and ASan/UBSan without diagnostics. Captures were visually
inspected. See `audits/renderer-cpu-opengl-update.md`.
No AMD/CachyOS execution, native Windows execution, manual minimize/focus testing, shader hot reload,
advanced rendering extensions, or general memory/security audit is claimed here.
Perform those checks when the relevant project stages and target machine are available.
## Sources and further reading
Course prose, scene/project, code, exercises, and week plans are original. These
sources document API contracts or provide an optional comparison:
- [GLFW getting started](https://www.glfw.org/docs/latest/quick.html) and
[context guide](https://www.glfw.org/docs/latest/context_guide.html).
- [Khronos OpenGL 3.3 core specification](https://registry.khronos.org/OpenGL/specs/gl/glspec33.core.pdf).
- [Khronos GLSL 3.30 specification](https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.3.30.pdf).
- [Apple's archived OpenGL guide and deprecation notice](https://developer.apple.com/opengl/).
- [C++ Core Guidelines: resource ownership](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rr-raii).
- [LearnOpenGL](https://learnopengl.com/): corresponding chapter links are attached to
the original course lessons; the learner does not need to leave the course to get
the explanations or progressive assignments.
This is the file included with the course. Copy and download give you its complete source.
Taking your work offline? Download all course companions
