docs/superpowers/skills/raylib-showcase-port-flow/SKILL.md
WS9 shipped 229 example ports (217 raylib core + 12 raygui) in 4 implementer waves over two days under a strict visual-parity rule: every Rust port mirrors its C counterpart's structure line-by-line. This skill captures the workflow that worked, plus the 15 specific lessons each wave's reviewer surfaced. Use it whenever new examples land upstream (raylib version bump, raygui re-vendor, gap fill) or when porting a fresh batch in a future session.
Core principle: The showcase's pedagogical value is the C↔Rust side-by-side viewer. A reader scrolling C-left / Rust-right needs lines to line up. Visual parity is the reviewer's primary criterion — refactoring into idiomatic Rust shapes is the most common rejection.
Use when any of the following triggers occur on the raylib-rs repo:
git diff <prev>..<new> -- examples/ shows added/renamed/removed .c files.showcase/build.rs hard-errors with C example <cat>/<name>.c has no Rust port at showcase/examples/<cat>/<name>.rs (the pairing check fired).raylib-sys/raygui-examples/ is re-pinned.Don't use for:
build.rs, viewer.rs, lib.rs, registry.rs, workflows). Those are out-of-scope for this flow.# Identify added/removed/renamed examples since the last bump
git -C raylib-sys/raylib diff <prev-tag>..<new-tag> -- examples/ | grep -E '^(diff|rename|new file|deleted)'
git -C raylib-sys/raygui-examples diff <prev-pin>..<new-pin> -- examples/
For each change:
[[example]] entry in showcase/Cargo.toml; rebuild.[[example]] entry, and any wasm-exclude.toml / thumbnails.toml lines.Open showcase/examples/core/core_basic_window.rs before writing anything. This is the canonical shape. Copy its structure verbatim: copyright header → use raylib::prelude::*; + use raylib_showcase::SourceViewer; → // Program main entry point banner → fn main() with Initialization / Main game loop / De-Initialization sections separated by C-style //---… banner lines.
Decide the dispatch shape per the WS9 pattern that worked:
| Batch size | Pattern |
|---|---|
| 1–2 examples | Port directly in the main session. |
| 3–15 examples in one category | One implementer subagent. One reviewer subagent. |
| Two ≤30-example categories | Two implementer subagents in parallel (they touch the same showcase/Cargo.toml — dispatch sequentially, not concurrently, to avoid write conflicts). Two reviewer subagents in parallel (read-only, safe). |
| One >30-example category | One implementer subagent that sub-batches internally (commits per sub-batch); one reviewer subagent at the end. |
Dispatch order rule: implementers are sequential per batch (Cargo.toml is a shared write target). Reviewers are parallel (read-only). A single fix subagent handling multiple batches' findings is fine.
For each .c source:
raylib-sys/raylib/examples/<cat>/<name>.c (or raylib-sys/raygui-examples/examples/<name>.c) side-by-side with the new Rust file.for (int i = 0; …) loops as for i in 0..n (or add a single // idiomatic: iter().map(...) comment if iterators read better — do not refactor).raylib::init(), RaylibHandle, &mut RaylibDrawHandle), but keep its layout aligned with the C.use raylib_showcase::SourceViewer; near use raylib::prelude::*;.let mut viewer = SourceViewer::for_current_example();.viewer.update(&mut rl, &thread); in the Update section, and viewer.draw(&mut d); at the end of the Draw section."resources/<file>"; the vendored copy is at showcase/resources/<category>/<file> — adjust the path string. The cwd when running cargo run -p raylib-showcase --example <name> is showcase/.Append to showcase/Cargo.toml:
[[example]]
name = "<name>"
path = "examples/<cat>/<name>.rs"
# For raygui examples only:
# required-features = ["raygui"]
cargo build -p raylib-showcase --example <name>
cargo build -p raylib-showcase --target wasm32-unknown-emscripten --example <name>
If wasm fails because of a legitimately desktop-only API → add to showcase/wasm-exclude.toml:
[[exclude]]
name = "<name>"
reason = "<one-line reason — LoadDroppedFiles / native dialog / threads / audio recording / etc.>"
If wasm fails because of a self-inflicted port problem → fix the port, don't wasm-exclude.
Dispatch a reviewer subagent with the reviewer brief (see Reference templates below). The reviewer returns APPROVED or REJECTED with per-example findings. Rejections come back to a fix subagent (or the implementer in the same session). Loop until clean.
git push fork 6.0-rc
# Watch on the fork:
# showcase.yml — 3-OS build + Linux wasm
# pages.yml — Pages deploy
gh run watch # or open https://github.com/Dacode45/ms-raylib-rs/actions
# Tag per wave/batch close:
git tag ws9-<phase>-<wave>-complete
git push fork ws9-<phase>-<wave>-complete
These are the specific failures the reviewer caught across the 4 waves. Each rule has a counter in the workflow above; they are listed here for direct lookup.
Visual parity is non-negotiable. Blank-line groupings, comment positions, init/loop/close structure must match the C. Translate comment text; preserve location. Do not collapse phases into closures/builders. Index-for loops stay index-based (or add a // idiomatic: … comment if iterators chosen). License/copyright header is verbatim.
The reference port to mirror is showcase/examples/core/core_basic_window.rs. Read it first before writing any new port. Copy its banner-comment layout exactly.
Viewer wiring is 3 lines, with TWO-arg viewer.update. The actual signature in core_basic_window.rs:57 is viewer.update(&mut rl, &thread). The plan's earlier brief template said one arg; that's stale. Future ports must use the two-arg form: viewer.update(&mut rl, &thread) + viewer.draw(&mut d).
Every unsafe { … } block MUST carry a // SAFETY: comment. Wave-2 and Wave-3 reviewers rejected ports without one. Two recurring SAFETY patterns:
// SAFETY: pure raylib FFI taking primitive args and returning a primitive; no aliasing or lifetime concerns.// SAFETY: <Owner> owns <ptr> as a <len>-long raylib-allocated array; the slice is read for this frame and the owner outlives the scope.Transmuting C magic numbers to typed Rust enums is UB. WS9 caught this twice (core_keyboard_testbed layout entry 162 not a KeyboardKey discriminant; image_exporter combo-index → PixelFormat). Fix: replace mem::transmute with an explicit match returning known-valid variants; gate placeholder values on a sentinel None return.
Cargo required-features must propagate everywhere. The first wave-1 push failed because xtask_wasm_build.rs and gen_thumbnails.rs ran cargo build --example <name> without consulting each example's required-features. raygui-gated examples failed with exit 101. Fix: parse showcase/Cargo.toml, build a name → required-features map, append --features X,Y to cargo for each example that needs them. Same fix in both xtask binaries.
GLSL_VERSION: mirror the upstream C cfg-fork, don't blanket-pin. Many raylib shader examples have BOTH glsl330 (desktop) and glsl100 (web) shaders via #if defined(PLATFORM_DESKTOP). The Rust port must mirror that:
#[cfg(target_family = "wasm")]
const GLSL_VERSION: i32 = 100;
#[cfg(not(target_family = "wasm"))]
const GLSL_VERSION: i32 = 330;
Wave 3 caught a shader sub-batch that blanket-pinned 330 and wasm-excluded 12 examples that didn't need to be excluded. Reviewer flagged; fixed in commit ee282d1.
wasm-exclude.toml is for examples that legitimately can't run on web — desktop-only APIs (LoadDroppedFiles, native file dialogs, threads, audio recording, screen recording / GIF encoders, VR stereo, MRT, gl_FragDepth, depth-texture sampling, compute shaders, filesystem polling, hot-reload). NOT for "I pinned glsl330 because I didn't want to deal with the cfg-fork." The acceptable list (from WS9 P2 close) is ~24 entries.
Subagent dispatch order.
showcase/Cargo.toml; parallel causes write conflicts).Implementer-brief outputs (every implementer subagent reports):
raylib/src/).Reviewer-brief outputs: APPROVED or REJECTED per example, with findings on:
// SAFETY: comments on every unsafe block.unsafe/FFI code.showcase/examples/<cat>/, showcase/Cargo.toml, showcase/wasm-exclude.toml, showcase/thumbnails.toml, and small raylib/src/ extensions noted in the implementer report).Resource paths in vendored layout. Upstream C references "resources/<file>"; the vendored copy is at showcase/resources/<category>/<file>. Translate paths in ports. The runtime cwd is showcase/.
Helper headers (controls_test_suite, custom_file_dialog, property_list, etc.). raygui examples sometimes pull in multi-hundred-line .h helpers (file dialog, property list, curve editor). Port simplified inline versions with // SIMPLIFIED: comments documenting the deviation. The reviewer accepts simplifications IF documented.
Strict pairing flag WS9_STRICT_PAIRING=1. Flipped on in .github/workflows/showcase.yml at P2 close (commit 635d146). New C examples added upstream will fail CI until a matching Rust port lands. Do not push partial state with the showcase/examples/raygui/ directory present but missing ports — build.rs will hard-error on every missing pair.
CI pattern: push to fork, watch the workflows, tag per wave. Push to the fork remote (Dacode45/ms-raylib-rs), watch showcase.yml (3-OS + Linux wasm) and pages.yml (deploys to dacode45.github.io/raylib-rs/). Tag per wave: ws9-p2-w1-complete, ws9-p2-w2-complete, …, ws9-p2-complete, ws9-p3-complete, ws9-p4-complete, ws9-complete. Never git push origin — canonical merge is the final-release workstream.
The implementer + reviewer briefs are the canonical text dispatched to subagents in each wave. Do not edit during a wave. Use verbatim; substitute <CATEGORY>.
Role: You are a Rust implementer porting raylib C examples to raylib-rs 6.0 under the visual-parity rule.
Scope: Port every
.cfile underraylib-sys/raylib/examples/<CATEGORY>/toshowcase/examples/<CATEGORY>/<name>.rs. Excluded:examples_template.c(already inbuild.rsEXEMPT_C).Reference port: open
showcase/examples/core/core_basic_window.rsfirst. Copy its banner-comment layout and viewer-wiring shape exactly.Visual-parity rule (the reviewer's primary criterion): Open the C file alongside the Rust file. Keep blank-line groupings, comment positions, and init/loop/close structure visually parallel. Translate comment text but preserve location. Do not collapse explicit init/update/draw phases into closures or builders. Do not replace index-
forloops with iterator chains for style alone (if iterators read genuinely better, leave the parallelforand add a one-line// idiomatic: …comment). Rust-only ergonomics worth a comment: RAII drop vs explicitUnload*,&strvs*const c_char,Result/?vs raw return-code checks, native math operator overloads vs raymath function calls.Per-example workflow:
- Read
raylib-sys/raylib/examples/<CATEGORY>/<name>.c.- Create
showcase/examples/<CATEGORY>/<name>.rsmirroring the C. Include the verbatim copyright/license banner at the top.- Add the three viewer-wiring lines:
use raylib_showcase::SourceViewer;near the otheruselines.- After init:
let mut viewer = SourceViewer::for_current_example();- Inside the loop:
viewer.update(&mut rl, &thread);in the Update section andviewer.draw(&mut d);at the end of the Draw section.- Add the
[[example]]entry toshowcase/Cargo.toml:toml(For raygui examples: add[[example]] name = "<name>" path = "examples/<CATEGORY>/<name>.rs"required-features = ["raygui"].)- Run
cargo build -p raylib-showcase --example <name>. Must pass.- Run
cargo build -p raylib-showcase --target wasm32-unknown-emscripten --example <name>. If it fails:
- If the failure is due to a wasm-incompatible raylib API (LoadDroppedFiles, native dialogs, threads, audio recording, etc.), add an entry to
showcase/wasm-exclude.toml:toml[[exclude]] name = "<name>" reason = "<one-line reason>"- If the failure is due to your own port code, fix the port code. Do NOT wasm-exclude as a workaround.
- For shader examples: mirror the upstream
#if defined(PLATFORM_DESKTOP)glsl330/glsl100 fork using#[cfg(target_family = "wasm")]/#[cfg(not(target_family = "wasm"))]. Do NOT blanket-pin 330 and then wasm-exclude.- If the default 60-frame thumbnail capture isn't representative, add a
thumbnails.tomloverride:tomlDefault is fine for most examples; only override when you've confirmed by running.[[example]] name = "<name>" frames = <higher number>- Every
unsafe { … }block MUST carry a// SAFETY:comment. Two recurring patterns:
- Pure FFI value-in/out:
// SAFETY: pure raylib FFI taking primitive args and returning a primitive; no aliasing or lifetime concerns.- raylib-owned array slice:
// SAFETY: <Owner> owns <ptr> as a <len>-long raylib-allocated array; the slice is read for this frame and the owner outlives the scope.- Never
mem::transmutea C magic number to a typed Rust enum. Replace with an explicitmatchreturning known-valid variants; sentinel placeholder values returnNone.- Resource paths: upstream C uses
"resources/<file>"; the vendored copy is atshowcase/resources/<CATEGORY>/<file>. Translate the path string. The runtime cwd isshowcase/.- Commit each example individually (or grouped where natural) with a message like
feat(ws9-pN): port <CATEGORY>/<name>.Safe-API gaps: If an example needs a raylib API surface that isn't wrapped in
raylib/, prefer (in order):
- Extend the safe API in a small focused change in
raylib/src/— fold into your port commit.- Use
unsafe { raylib::ffi::… }inline with a// SAFETY: …comment.- Escalate to the reviewer if the surface is large.
Do not edit:
build.rs,viewer.rs,lib.rs,registry.rs, the workflows, or anything outsideshowcase/examples/<CATEGORY>/,showcase/Cargo.toml,showcase/wasm-exclude.toml,showcase/thumbnails.toml, and the smallraylib/src/extensions noted above.Output: report (a) count ported / total in category, (b) wasm-exclusions added with reason, (c) thumbnails.toml overrides added with reason, (d) safe-API extensions made (file + summary), (e) commit SHAs, (f) any examples you couldn't port and your reasoning.
Role: You are a reviewer for raylib-rs showcase ports under the visual-parity rule. A previous subagent ported category
<CATEGORY>.What to check per example file:
- Visual parity (primary criterion): Open
raylib-sys/raylib/examples/<CATEGORY>/<name>.candshowcase/examples/<CATEGORY>/<name>.rsside by side.
- Blank-line groupings match.
- Comment positions match (translated text, same location).
- Init → loop → close structure is parallel.
- No collapse of phases into closures/builders.
- For-loops that were index-based in C are still index-based in Rust (or have a
// idiomatic: …comment if iterators were chosen).- License/copyright banner is verbatim from the C original.
- Three viewer-wiring lines are present and correctly placed:
use raylib_showcase::SourceViewer;,let mut viewer = SourceViewer::for_current_example();after init,viewer.update(&mut rl, &thread);+viewer.draw(&mut d);inside the loop.viewer.updateis the two-arg form.- Every
unsafe { … }block carries a// SAFETY:comment that names the soundness reason.- No
mem::transmuteof C magic numbers to typed Rust enums (UB). Match-based mapping with sentinelNoneis the accepted pattern.[[example]]entry inshowcase/Cargo.tomlmatchesname+path. For raygui examples:required-features = ["raygui"]is present.cargo build -p raylib-showcase --example <name>is clean.cargo build -p raylib-showcase --target wasm32-unknown-emscripten --example <name>is clean or the example is inwasm-exclude.tomlwith a legitimate desktop-only reason. Shader examples that blanket-pinned glsl330 and wasm-excluded as the workaround are rejected.- No edits outside the allowed paths:
showcase/examples/<CATEGORY>/,showcase/Cargo.toml,showcase/wasm-exclude.toml,showcase/thumbnails.toml, plus anyraylib/src/extensions noted in the implementer report.What to do with violations:
- Visual-parity violation → flag with file:line + suggested fix; do NOT fix yourself (kick back to implementer).
- Build failure → flag; the implementer dispatches a fix.
- Missing
// SAFETY:or transmute-UB → flag with the specific concern; do NOT fix yourself.- Style nit that doesn't affect parity → note but accept.
Output:
APPROVEDorREJECTEDper example with findings. If rejected, the implementer fixes and the reviewer re-checks just the fixed files.
docs/superpowers/specs/2026-05-31-ws9-showcase-design.md — Section 14 has the canonical visual-parity rule; Section 6 has the architecture; Section 8 has data flows including Flow F (future raylib bump, the flow this skill formalizes).docs/superpowers/plans/2026-06-01-ws9-showcase.md — Task 2.0 has the original brief templates; Tasks 2.1–2.4 are the four-wave dispatch shape.showcase/examples/core/core_basic_window.rs — the canonical structure to mirror.ecfc03d (audio) + 0c03b52 (audio) + 6d9b6c3 (others) + 4147f51 (required-features fix).e867a39 (core) + b1a00a5 (text) + 9a403ae (transmute-UB fix) + feb5c4e (SAFETY-comment backfill).c550ab0 (models) + 0a5f56d + 021032d + ee282d1 (shader GLSL_VERSION cfg-fork fix).78edf3c (shapes) + 9cbfaa9 (textures) + cedb550 + d90d70f.635d146 (WS9_STRICT_PAIRING=1 flipped on).ae97503 (raygui complete) + 5e143d9 (image_exporter transmute-UB fix).showcase-c-rust-port-style — the visual-parity rule itself.examples-not-bins — why [[example]] not [[bin]].rgui-feature-gate-rule — #[cfg(feature = "raygui")] discipline (rare for examples since required-features already gates them; matters for any helper added to the lib crate).skip-std-equivalent-fns — when an example calls a raylib API that Rust std handles, prefer std; don't extend the safe API for std-equivalents.