From 88bc633dacb11008c05efed5fb6a6d63c7cd8ae5 Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Fri, 11 Sep 2026 01:16:25 +0200 Subject: [PATCH 1/2] Run Windows CI through Bash and repair example palette imports --- .github/workflows/test.yml | 22 ++++++ docs/evidence/windows-example-ci-v1.md | 45 ++++++++++++ docs/windows-engine-plan.md | 99 ++++++++------------------ examples/dungeon-crawl/main.ts | 14 ++-- examples/isometric-rpg/main.ts | 28 ++++---- examples/kart-racer/main.ts | 22 +++--- examples/pong/main.ts | 18 ++--- examples/space-blaster/main.ts | 16 ++--- examples/voxel-sandbox/main.ts | 12 ++-- src/core/colors.ts | 6 +- tools/check-ci-contract.js | 10 +++ 11 files changed, 162 insertions(+), 130 deletions(-) create mode 100644 docs/evidence/windows-example-ci-v1.md diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cb8ece54..651e05e7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,12 @@ concurrency: group: test-${{ github.ref }} cancel-in-progress: true +defaults: + run: + # The shared entry point is Bash. PowerShell can return success after + # opening a .sh file without executing or waiting for its commands. + shell: bash + env: CARGO_TERM_COLOR: always # Pin macOS deployment target so cached object files carry a stable @@ -73,6 +79,14 @@ jobs: RUST_TEST_NOCAPTURE: "1" run: ./scripts/ci-check.sh --quick --component shared-tests + - name: Retain executed shared-check summary + if: always() + uses: actions/upload-artifact@v4 + with: + name: shared-check-summary-${{ matrix.os }} + path: target/ci/quick-shared-tests.json + if-no-files-found: error + - name: Upload failure evidence if: failure() uses: ./.github/actions/upload-ci-failure @@ -235,6 +249,14 @@ jobs: - name: full / host-build run: ./scripts/ci-check.sh --full --component host-build + - name: Retain executed Windows-build summary + if: always() + uses: actions/upload-artifact@v4 + with: + name: windows-build-summary + path: target/ci/full-host-build.json + if-no-files-found: error + - name: Upload failure evidence if: failure() uses: ./.github/actions/upload-ci-failure diff --git a/docs/evidence/windows-example-ci-v1.md b/docs/evidence/windows-example-ci-v1.md new file mode 100644 index 00000000..4366cc21 --- /dev/null +++ b/docs/evidence/windows-example-ci-v1.md @@ -0,0 +1,45 @@ +# Windows example linking and CI execution + +A native compile-and-link audit of all 20 canonical examples at `d610d6a` +produced 13 successful Windows executables and seven failures with the pinned +official Perry 0.5.1182 toolchain. This exposed failures that an inventory check +or `--no-link` compilation cannot establish as passing native builds. + +Six examples still accessed `Color.White`, `Color.Red`, and other palette values +after `bloom/core` stopped exporting `Color` as a value. `Color` remains the RGBA +type; `Colors` is the public palette. Correcting those imports and accesses +makes Pong, Dungeon Crawl, Isometric RPG, Kart Racer, Space Blaster, and Voxel +Sandbox compile and link on the same compiler. Type annotations retain `Color`. +No renderer or library API changed. + +The remaining `perry-embed` failure is independent. Perry 0.5.1182 does not +support the example's `bloomViewGetNativeHandle` call. Official 0.5.1219 supports +that API but its prebuilt standard library fails to link missing HTTP extension +symbols. Official 0.5.1220 had already exposed the same standard-library issue +in the original #153 work. The embedded-view example remains unqualified; +compiler source/build investigation continues rather than removing the example +from the required inventory. + +## Hosted Windows execution gap + +The #159 Windows shared-test job reported success in about one second. Its +retained log identifies PowerShell as the shell, invokes `ci-check.sh`, then +proceeds to cleanup without any Cargo output or test results. The cache action +also reports that its build paths do not exist. That status does not prove test +execution. Local Windows shared tests were run explicitly through Bash and +Cargo, so their previously published execution evidence remains valid. + +The Tests workflow now selects Bash explicitly and requires the emitted shared +test summary on every host and the native-build summary on Windows. Artifact +upload fails when a summary is absent, making another silent non-execution a +failure. The CI command contract checks both the shell and required evidence. +Hosted execution on this correction still needs verification from actual Cargo +output and summary contents. + +Audit commands, original failure logs, compiler-release metadata, source patch, +and the six corrected executable hashes are retained under +`tools/quality/out/windows-engine-plan/all-examples/`. The original audit is also +included in the [#159 evidence release](https://github.com/Bloom-Engine/engine/releases/tag/quality-evidence-ssgi-surface-20260911). + +This is progress on #140/#142/#74. All-example PR compilation, real starter and +embedded-view startup, and clean package installation are still required. diff --git a/docs/windows-engine-plan.md b/docs/windows-engine-plan.md index 6297f121..89951dfc 100644 --- a/docs/windows-engine-plan.md +++ b/docs/windows-engine-plan.md @@ -15,11 +15,11 @@ first nine-scene Radeon evidence are in draft PR #154. Follow-up work starts at | Work | Required completion evidence | Current state | | --- | --- | --- | | #127 Vulkan PT correctness | Three deterministic progressive and motion runs, both negative controls, finite intermediates, reset/lighting/rigid-motion checks, retained report | Canonical hardware gate, all four focused temporal tests, and CPU reference sanity check pass on Radeon/Vulkan; [report](evidence/issue-127-windows-vulkan-v1.md) and [raw evidence](https://github.com/Bloom-Engine/engine/releases/tag/quality-evidence-155-windows-vulkan-20260910) published | -| #128 Windows image discrepancies | Identify the first incorrect stage or document a reviewed backend-specific baseline decision; rerun the full strict corpus and reproducibility checks | [Cutout phase correction](evidence/windows-alpha-phase-v1.md) passes all nine Radeon image gates in two complete runs and both hosted Metal images. Reproducibility passes with 257 byte-identical artifacts. Both strict runs fail postflight host-load checks, so their timing remains unqualified | -| #135 / #149 temporal reconstruction | Enforced motion/producer/quality-preset corpus, representative scenes, fractional/native and frozen A/B timing, memory/resize checks, platform evidence | Device/resource, stationary SSGI, and profiler fixes are retained. The [surface reconstruction correction](evidence/windows-ssgi-surface-v1.md) passes the original HD startup limits on Radeon/Vulkan; all 93 local goldens that run pass, including lighting recovery. Full scene-image, timing, and platform qualification of that correction remain open, as does the wider representative corpus | -| #140 integration gates | Same required local/hosted lanes pass on exact source; release package startup and all-example evidence | All 24 hosted checks pass at #158 source `662a44f`, including macOS shared/golden tests, mobile target builds, native/web builds, browser startup, and canonical Metal images. Scheduled physical-hardware checks, all-example compilation, and release-install acceptance remain separate requirements | +| #128 Windows image discrepancies | Identify the first incorrect stage or document a reviewed backend-specific baseline decision; rerun the full strict corpus and reproducibility checks | Cutout and surface corrections pass all nine Radeon images. At #159 source `d610d6a`, full runs 2 and 3 pass every configured check and reproduce 257 artifacts byte-identically with matching metadata and timing differences inside existing noise bounds. Earlier invalid runs retain their failures; named hardware acceptance remains separate | +| #135 / #149 temporal reconstruction | Enforced motion/producer/quality-preset corpus, representative scenes, fractional/native and frozen A/B timing, memory/resize checks, platform evidence | Device/resource, stationary SSGI, and profiler fixes are retained. The surface correction passes original HD startup limits and 154,720 analytic receiver checks on Vulkan, DX12, and hosted Metal; 93 local goldens pass, including lighting recovery. The full Radeon corpus passes twice. Wider representative scenes, frozen A/B performance, memory/resize, and platform acceptance remain open | +| #140 integration gates | Same required local/hosted lanes pass on exact source; release package startup and all-example evidence | #159 has 24 successful CheckRun statuses, but Windows logs reveal no Cargo execution because Bash was invoked through PowerShell without an explicit shell. Local Windows execution is verified. This follow-up selects Bash and requires execution-summary artifacts; actual hosted validation is pending. All-example PR compilation and release-install acceptance remain separate requirements | | #138 capability fallback | Actual constrained-adapter startup and relevant forced-tier corpus, truthful capability outputs | Existing implementation/evidence preserved; physical constrained-limit acceptance still needs proof | -| PR integration | Reviewable changes, passing required checks, full issue evidence, merge-ready rendering branch | #147 and the stacked fixes #154–#158 remain drafts; no merge performed | +| PR integration | Reviewable changes, passing required checks, full issue evidence, merge-ready rendering branch | #147 and the stacked fixes #154–#159 remain drafts; no merge performed | ## Engine work retained in scope @@ -54,71 +54,28 @@ audit are saved in `tools/quality/out/windows-engine-plan/plan-requirements.json ## Current next steps -1. #155 source `64d5eed` has passing hosted checks and published evidence. The - archive SHA-256 is `de9c1beca73bfcf60bf79d3a612b72c072f726f6272576a7f0f60ffdbc7ce25d`. - CI run URLs and conclusions are in its separate `pr155-checks-64d5eed.json` - release asset. No draft PR has been merged. -2. The [profiler correction](evidence/windows-profiler-integrity-v1.md) on - `codex/windows-profiler-integrity` passes local contracts, lint, the quality - lane, and the complete shared suite. Its current-frame regression rejects - all 12 old Vulkan samples and passes with the correction on Vulkan and DX12. - Corrected SSGI timing covers 20 isolated runs, each with 120 complete GPU - frames. Hosted Metal's shared lane passes, but its Apple Paravirtual adapter - lacks timestamp queries: the profiler GPU regression explicitly skips. - The retained `--nocapture` log at `1965a0b` confirms this; a test reported as - "ok" after that early return does not qualify Metal GPU timing. - Its colored-shadow - failure exposed an [inverse-matrix upload defect](evidence/windows-transmitted-shadow-inverse-vp-v1.md); - the correction passes the isolated local check and rejects the wrong-color - control. At follow-up source `fa93690`, all 23 hosted checks and the complete - local shared suite pass. The shadow regression passes on Metal. The shadow archive - and CI receipt are published alongside the immutable profiler archive. -3. Diagnose Sponza and skinned/alpha against the portable baselines, then repair - HD temporal stability and complete the representative temporal/geometry - corpus. Recapture affected timing evidence with explicit coverage fields. - Disabling foliage shadow casting retains the Windows skinned/alpha mismatch; - canonical captures at `98cce62` pass on hosted Metal with SSIM 0.997442544 for - Sponza and 0.999417603 for skinned/alpha. That Apple Paravirtual adapter uses - the modern tier and software GI and exposes no timestamps. At `0dd8f67`, - [PR #157](https://github.com/Bloom-Engine/engine/pull/157) has all 24 hosted - checks passing; both Metal images pass with raw export enabled. The - [published diagnostic archive](https://github.com/Bloom-Engine/engine/releases/tag/quality-evidence-image-portability-20260910) - retains exact depth and MRT bytes, commands, checksums, and source identity. - On matching modern/software-GI paths, Windows still fails both images. - Skinned/alpha has 9,745 depth coverage disagreements before TAA, while - albedo RGB closely agrees on matching surfaces. Disabling foliage shadows - and an isolated isotropic alpha-sampling control retain the failure. - Exact cutout-input probes at `30e7625` identify a different Bayer phase - extent: Metal's observed extent predicts every inspected threshold away from - integer LOD boundaries. The [integer phase correction](evidence/windows-alpha-phase-v1.md) - preserves that approved grid on both backends. Focused Windows images now - pass at SSIM 0.986160457 and 0.990073442. At `662a44f`, all nine Radeon image - gates pass twice and reproducibility passes with 257 byte-identical artifacts. - Both full runs fail their unchanged postflight host-load checks: System CPU - exceeds the 75% per-process limit on Bistro, and in the second run also on - draw/light stress and weighted transparency. Those timing windows remain - unqualified. The [#158 evidence archive](https://github.com/Bloom-Engine/engine/releases/tag/quality-evidence-alpha-phase-20260910) - retains both failed strict runs and their separate passing image/repro checks. - All 24 hosted checks pass; the new cutout GPU regression actually executes on - Vulkan, DX12, and Metal. Raw export leaves unmodified Windows final PNGs byte-identical. - Shared-runner timing cannot qualify hardware budgets. - The HD TAA fixture originally failed at its required 16-frame warm-up and - passed diagnostic controls at 32, 64, and 128 frames. The follow-up - [surface correction](evidence/windows-ssgi-surface-v1.md) repairs depth-texel - coordinates and resolution-dependent normal reconstruction. It passes the - original 16-frame HD limits, removes the observed horizontal GI bands, and - preserves the existing lighting-recovery regression on Radeon/Vulkan. - Both isolated partial corrections fail. Full scene and platform checks of - the combined change remain in progress; no threshold or warm-up was relaxed. -4. Continue starter/all-example and release-install checks, asset/world streaming, - schema-generated APIs, components, and runtime UI against each issue's full - acceptance criteria. Hardware-specific acceptance remains open while local - work progresses. +1. Verify actual hosted Windows test and build execution. The + [CI and example correction](evidence/windows-example-ci-v1.md) explicitly + selects Bash and requires execution-summary artifacts. Its hosted run must + show Cargo output and successful summary contents before this gap is closed. +2. Finish all-example native linking, real starter/example startup, and clean + Windows installation. Six palette corrections bring the local link audit + from 13 to 19 successful examples out of 20. The embedded-view example needs + a newer Perry API, and both available newer Windows bundles have an + independent standard-library link failure that still needs resolution. + All-example compilation must become a required PR check. +3. Complete the wider temporal/geometry, performance, memory, resize, and + capability corpus. The + [HD surface correction](evidence/windows-ssgi-surface-v1.md) and two valid + full Radeon runs are [published with #159](https://github.com/Bloom-Engine/engine/releases/tag/quality-evidence-ssgi-surface-20260911). + All nine images pass; reproducibility retains 257 byte-identical artifacts. + The original 16-frame HD startup limits pass on Vulkan, DX12, and hosted + Metal. Earlier host-load failures remain invalid timing windows. Hosted + Metal lacks timestamp queries and cannot qualify GPU timing. +4. Complete API generation, streaming, components, runtime UI, and packaging + against the full issue requirements above, then prepare the draft stack for + review and integration. These outcomes include both implementation work and + acceptance evidence; they do not imply that every subsystem is absent. -The immediate order is to repair the HD temporal startup failure, obtain valid -full Radeon timing windows, and finish all-example startup and clean Windows -installation checks. Then complete the wider temporal/geometry corpus and the -engine API, streaming, component, and UI requirements above. These remaining -outcomes include both implementation work and acceptance evidence; they are not -a claim that each subsystem is absent. None requires waiting for an RTX 4080 to -continue local work. +Local work continues on the Radeon 760M. RTX-specific, physical constrained +adapter, and other unavailable hardware acceptance remains explicitly open. diff --git a/examples/dungeon-crawl/main.ts b/examples/dungeon-crawl/main.ts index 7f1e48f6..98d31f59 100644 --- a/examples/dungeon-crawl/main.ts +++ b/examples/dungeon-crawl/main.ts @@ -3,7 +3,7 @@ import { clearBackground, setTargetFPS, getDeltaTime, isKeyPressed, isKeyDown, getScreenWidth, getScreenHeight, closeWindow, beginMode2D, endMode2D, } from "bloom/core"; -import { Color, Key, Camera2D } from "bloom/core"; +import { Color, Colors, Key, Camera2D } from "bloom/core"; import { drawRect, drawCircle, drawRectLines } from "bloom/shapes"; import { drawText, measureText } from "bloom/text"; import { clamp, randomInt, randomFloat } from "bloom/math"; @@ -383,7 +383,7 @@ while (!windowShouldClose()) { ); // HP bar const hpRatio = enemies[i].hp / enemies[i].maxHp; - drawRect(enemies[i].x * TILE_SIZE, enemies[i].y * TILE_SIZE - 4, Math.floor(TILE_SIZE * hpRatio), 3, Color.Red); + drawRect(enemies[i].x * TILE_SIZE, enemies[i].y * TILE_SIZE - 4, Math.floor(TILE_SIZE * hpRatio), 3, Colors.Red); } // Draw player @@ -398,9 +398,9 @@ while (!windowShouldClose()) { // HUD drawRect(0, 0, SCREEN_WIDTH, 35, { r: 0, g: 0, b: 0, a: 180 }); - drawText("HP: " + player.hp.toString() + "/" + player.maxHp.toString(), 10, 8, 20, player.hp > player.maxHp / 3 ? Color.Green : Color.Red); - drawText("Floor: " + floor.toString(), 200, 8, 20, Color.White); - drawText("Turns: " + turnCount.toString(), 350, 8, 20, Color.LightGray); + drawText("HP: " + player.hp.toString() + "/" + player.maxHp.toString(), 10, 8, 20, player.hp > player.maxHp / 3 ? Colors.Green : Colors.Red); + drawText("Floor: " + floor.toString(), 200, 8, 20, Colors.White); + drawText("Turns: " + turnCount.toString(), 350, 8, 20, Colors.LightGray); // Message log if (messageTimer > 0) { @@ -412,9 +412,9 @@ while (!windowShouldClose()) { if (player.hp <= 0) { drawRect(0, SCREEN_HEIGHT / 2 - 50, SCREEN_WIDTH, 100, { r: 0, g: 0, b: 0, a: 200 }); const deathMsg = "You have perished on floor " + floor.toString(); - drawText(deathMsg, SCREEN_WIDTH / 2 - measureText(deathMsg, 24) / 2, SCREEN_HEIGHT / 2 - 20, 24, Color.Red); + drawText(deathMsg, SCREEN_WIDTH / 2 - measureText(deathMsg, 24) / 2, SCREEN_HEIGHT / 2 - 20, 24, Colors.Red); const restartMsg = "Press ENTER to try again"; - drawText(restartMsg, SCREEN_WIDTH / 2 - measureText(restartMsg, 18) / 2, SCREEN_HEIGHT / 2 + 15, 18, Color.LightGray); + drawText(restartMsg, SCREEN_WIDTH / 2 - measureText(restartMsg, 18) / 2, SCREEN_HEIGHT / 2 + 15, 18, Colors.LightGray); } endDrawing(); diff --git a/examples/isometric-rpg/main.ts b/examples/isometric-rpg/main.ts index f8193d6c..76d8a470 100644 --- a/examples/isometric-rpg/main.ts +++ b/examples/isometric-rpg/main.ts @@ -5,7 +5,7 @@ import { getMouseX, getMouseY, isMouseButtonPressed, writeFile, fileExists, } from "bloom/core"; -import { Color, Key, Camera2D, MouseButton } from "bloom/core"; +import { Color, Colors, Key, Camera2D, MouseButton } from "bloom/core"; import { drawRect, drawRectLines, drawCircle, drawTriangle, checkCollisionPointRec, @@ -138,7 +138,7 @@ function itemColor(t: number): Color { if (t === ITEM_SHIELD) return { r: 100, g: 100, b: 200, a: 255 }; if (t === ITEM_KEY) return { r: 255, g: 220, b: 50, a: 255 }; if (t === ITEM_COIN) return { r: 255, g: 200, b: 0, a: 255 }; - return Color.White; + return Colors.White; } function itemName(t: number): string { @@ -409,7 +409,7 @@ while (!windowShouldClose()) { // HP bar const barW = TILE_W * camera.zoom * 0.6; const hpRatio = npcs[i].hp / npcs[i].maxHp; - drawRect(sx - barW / 2, sy - size - 4 + TILE_H * camera.zoom * 0.3, barW * hpRatio, 3, Color.Red); + drawRect(sx - barW / 2, sy - size - 4 + TILE_H * camera.zoom * 0.3, barW * hpRatio, 3, Colors.Red); } // Draw player @@ -424,15 +424,15 @@ while (!windowShouldClose()) { // HUD panel drawRect(0, 0, SCREEN_WIDTH, 45, { r: 20, g: 20, b: 30, a: 220 }); - drawText(player.name + " Lv." + level.toString(), 10, 5, 18, Color.White); + drawText(player.name + " Lv." + level.toString(), 10, 5, 18, Colors.White); // HP bar drawRect(10, 28, 120, 10, { r: 60, g: 0, b: 0, a: 255 }); - drawRect(10, 28, Math.floor(120 * player.hp / player.maxHp), 10, Color.Red); - drawText(player.hp.toString() + "/" + player.maxHp.toString(), 15, 27, 10, Color.White); + drawRect(10, 28, Math.floor(120 * player.hp / player.maxHp), 10, Colors.Red); + drawText(player.hp.toString() + "/" + player.maxHp.toString(), 15, 27, 10, Colors.White); drawText("ATK: " + player.attack.toString(), 150, 8, 16, { r: 255, g: 150, b: 50, a: 255 }); drawText("DEF: " + player.defense.toString(), 240, 8, 16, { r: 50, g: 150, b: 255, a: 255 }); - drawText("Gold: " + gold.toString(), 330, 8, 16, Color.Yellow); + drawText("Gold: " + gold.toString(), 330, 8, 16, Colors.Yellow); drawText("EXP: " + exp.toString() + "/" + (level * 20).toString(), 430, 8, 16, { r: 150, g: 255, b: 150, a: 255 }); // Inventory @@ -442,17 +442,17 @@ while (!windowShouldClose()) { if (i > 0) invStr = invStr + ", "; invStr = invStr + itemName(inventory[i]); } - drawText(invStr, 550, 8, 14, Color.LightGray); + drawText(invStr, 550, 8, 14, Colors.LightGray); } // Dialogue box if (showDialogue) { drawRect(50, SCREEN_HEIGHT - 120, SCREEN_WIDTH - 100, 100, { r: 10, g: 10, b: 30, a: 230 }); - drawRectLines(50, SCREEN_HEIGHT - 120, SCREEN_WIDTH - 100, 100, 2, Color.White); + drawRectLines(50, SCREEN_HEIGHT - 120, SCREEN_WIDTH - 100, 100, 2, Colors.White); const npcName = npcs[dialogueNpc].name; - drawText(npcName, 70, SCREEN_HEIGHT - 110, 20, Color.Yellow); - drawText(dialogueText, 70, SCREEN_HEIGHT - 80, 18, Color.White); - drawText("[SPACE] to continue", 70, SCREEN_HEIGHT - 35, 14, Color.LightGray); + drawText(npcName, 70, SCREEN_HEIGHT - 110, 20, Colors.Yellow); + drawText(dialogueText, 70, SCREEN_HEIGHT - 80, 18, Colors.White); + drawText("[SPACE] to continue", 70, SCREEN_HEIGHT - 35, 14, Colors.LightGray); } // Message log @@ -464,8 +464,8 @@ while (!windowShouldClose()) { // Death if (player.hp <= 0) { drawRect(0, SCREEN_HEIGHT / 2 - 40, SCREEN_WIDTH, 80, { r: 0, g: 0, b: 0, a: 200 }); - drawText("YOU DIED", SCREEN_WIDTH / 2 - measureText("YOU DIED", 50) / 2, SCREEN_HEIGHT / 2 - 30, 50, Color.Red); - drawText("Press ENTER to respawn", SCREEN_WIDTH / 2 - measureText("Press ENTER to respawn", 18) / 2, SCREEN_HEIGHT / 2 + 25, 18, Color.LightGray); + drawText("YOU DIED", SCREEN_WIDTH / 2 - measureText("YOU DIED", 50) / 2, SCREEN_HEIGHT / 2 - 30, 50, Colors.Red); + drawText("Press ENTER to respawn", SCREEN_WIDTH / 2 - measureText("Press ENTER to respawn", 18) / 2, SCREEN_HEIGHT / 2 + 25, 18, Colors.LightGray); } // Controls hint diff --git a/examples/kart-racer/main.ts b/examples/kart-racer/main.ts index c4886f1d..619ea1e7 100644 --- a/examples/kart-racer/main.ts +++ b/examples/kart-racer/main.ts @@ -3,7 +3,7 @@ import { clearBackground, setTargetFPS, getDeltaTime, isKeyDown, isKeyPressed, closeWindow, beginMode3D, endMode3D, } from "bloom/core"; -import { Color, Key, Camera3D } from "bloom/core"; +import { Color, Colors, Key, Camera3D } from "bloom/core"; import { drawCube, drawPlane, drawSphere, drawCubeWires, drawGrid, } from "bloom/models"; @@ -305,12 +305,12 @@ while (!windowShouldClose()) { // Start/finish line const startCp = checkpoints[0]; - drawCube({ x: startCp.x, y: 0.2, z: startCp.z }, 1, 0.3, TRACK_WIDTH, Color.White); + drawCube({ x: startCp.x, y: 0.2, z: startCp.z }, 1, 0.3, TRACK_WIDTH, Colors.White); // Checkpoint markers (small posts on track edges) for (let i = 0; i < NUM_CHECKPOINTS; i++) { const cp = checkpoints[i]; - const color = i === 0 ? Color.White : { r: 200, g: 200, b: 50, a: 255 }; + const color = i === 0 ? Colors.White : { r: 200, g: 200, b: 50, a: 255 }; drawCube({ x: cp.x, y: 1, z: cp.z - TRACK_WIDTH * 0.5 - 1 }, 0.5, 2, 0.5, color); drawCube({ x: cp.x, y: 1, z: cp.z + TRACK_WIDTH * 0.5 + 1 }, 0.5, 2, 0.5, color); } @@ -357,21 +357,21 @@ while (!windowShouldClose()) { // Speed const speedKmh = Math.floor(Math.abs(player.speed) * 3.6); - drawText(speedKmh.toString() + " km/h", 10, 10, 22, Color.White); + drawText(speedKmh.toString() + " km/h", 10, 10, 22, Colors.White); // Position - drawText(placeSuffix(playerPlace), 200, 10, 22, Color.Yellow); + drawText(placeSuffix(playerPlace), 200, 10, 22, Colors.Yellow); // Lap const lapText = "Lap " + Math.min(player.lap + 1, TOTAL_LAPS).toString() + "/" + TOTAL_LAPS.toString(); - drawText(lapText, 350, 10, 22, Color.White); + drawText(lapText, 350, 10, 22, Colors.White); // Time - drawText(formatTime(raceTime), SCREEN_WIDTH - 150, 10, 22, Color.LightGray); + drawText(formatTime(raceTime), SCREEN_WIDTH - 150, 10, 22, Colors.LightGray); // Best lap if (bestLapTime > 0) { - drawText("Best: " + formatTime(bestLapTime), SCREEN_WIDTH - 150, 35, 16, Color.Green); + drawText("Best: " + formatTime(bestLapTime), SCREEN_WIDTH - 150, 35, 16, Colors.Green); } // Countdown @@ -380,16 +380,16 @@ while (!windowShouldClose()) { const countText = countNum > 0 ? countNum.toString() : "GO!"; const fontSize = 80; drawText(countText, SCREEN_WIDTH / 2 - measureText(countText, fontSize) / 2, SCREEN_HEIGHT / 2 - 50, fontSize, - countNum <= 1 ? Color.Green : Color.Red); + countNum <= 1 ? Colors.Green : Colors.Red); } // Race finish if (raceFinished) { drawRect(0, SCREEN_HEIGHT / 2 - 60, SCREEN_WIDTH, 120, { r: 0, g: 0, b: 0, a: 200 }); const finishText = "RACE COMPLETE!"; - drawText(finishText, SCREEN_WIDTH / 2 - measureText(finishText, 50) / 2, SCREEN_HEIGHT / 2 - 45, 50, Color.Gold); + drawText(finishText, SCREEN_WIDTH / 2 - measureText(finishText, 50) / 2, SCREEN_HEIGHT / 2 - 45, 50, Colors.Gold); const resultText = "Finished " + placeSuffix(playerPlace) + " — Time: " + formatTime(raceTime); - drawText(resultText, SCREEN_WIDTH / 2 - measureText(resultText, 24) / 2, SCREEN_HEIGHT / 2 + 15, 24, Color.White); + drawText(resultText, SCREEN_WIDTH / 2 - measureText(resultText, 24) / 2, SCREEN_HEIGHT / 2 + 15, 24, Colors.White); } // Controls hint (first few seconds) diff --git a/examples/pong/main.ts b/examples/pong/main.ts index a8ec391c..f5e8588d 100644 --- a/examples/pong/main.ts +++ b/examples/pong/main.ts @@ -3,7 +3,7 @@ import { clearBackground, setTargetFPS, getDeltaTime, isKeyDown, getScreenWidth, getScreenHeight, closeWindow, } from "bloom/core"; -import { Color, Key } from "bloom/core"; +import { Colors, Key } from "bloom/core"; import { drawRect, drawCircle, checkCollisionRecs } from "bloom/shapes"; import { drawText, measureText } from "bloom/text"; import { initAudioDevice, loadSound, playSound, closeAudioDevice } from "bloom/audio"; @@ -130,7 +130,7 @@ while (!windowShouldClose()) { // Drawing beginDrawing(); - clearBackground(Color.Black); + clearBackground(Colors.Black); // Center line const segments = 20; @@ -141,27 +141,27 @@ while (!windowShouldClose()) { i * segHeight * 2, 2, segHeight, - Color.DarkGray, + Colors.DarkGray, ); } // Paddles - drawRect(PADDLE_MARGIN, leftPaddleY, PADDLE_WIDTH, PADDLE_HEIGHT, Color.White); - drawRect(SCREEN_WIDTH - PADDLE_MARGIN - PADDLE_WIDTH, rightPaddleY, PADDLE_WIDTH, PADDLE_HEIGHT, Color.White); + drawRect(PADDLE_MARGIN, leftPaddleY, PADDLE_WIDTH, PADDLE_HEIGHT, Colors.White); + drawRect(SCREEN_WIDTH - PADDLE_MARGIN - PADDLE_WIDTH, rightPaddleY, PADDLE_WIDTH, PADDLE_HEIGHT, Colors.White); // Ball - drawCircle(ballX, ballY, BALL_RADIUS, Color.White); + drawCircle(ballX, ballY, BALL_RADIUS, Colors.White); // Scores const leftScoreText = leftScore.toString(); const rightScoreText = rightScore.toString(); - drawText(leftScoreText, SCREEN_WIDTH / 4 - measureText(leftScoreText, 40) / 2, 20, 40, Color.White); - drawText(rightScoreText, 3 * SCREEN_WIDTH / 4 - measureText(rightScoreText, 40) / 2, 20, 40, Color.White); + drawText(leftScoreText, SCREEN_WIDTH / 4 - measureText(leftScoreText, 40) / 2, 20, 40, Colors.White); + drawText(rightScoreText, 3 * SCREEN_WIDTH / 4 - measureText(rightScoreText, 40) / 2, 20, 40, Colors.White); // Pause text if (paused) { const pauseText = "PAUSED"; - drawText(pauseText, SCREEN_WIDTH / 2 - measureText(pauseText, 30) / 2, SCREEN_HEIGHT / 2 - 15, 30, Color.LightGray); + drawText(pauseText, SCREEN_WIDTH / 2 - measureText(pauseText, 30) / 2, SCREEN_HEIGHT / 2 - 15, 30, Colors.LightGray); } endDrawing(); diff --git a/examples/space-blaster/main.ts b/examples/space-blaster/main.ts index c27e776f..a93436a5 100644 --- a/examples/space-blaster/main.ts +++ b/examples/space-blaster/main.ts @@ -3,7 +3,7 @@ import { clearBackground, setTargetFPS, getDeltaTime, isKeyDown, isKeyPressed, getScreenWidth, getScreenHeight, closeWindow, } from "bloom/core"; -import { Color, Key } from "bloom/core"; +import { Color, Colors, Key } from "bloom/core"; import { drawRect, drawCircle, drawTriangle, checkCollisionRecs, checkCollisionCircleRec } from "bloom/shapes"; import { drawText, measureText } from "bloom/text"; import { initAudioDevice, closeAudioDevice } from "bloom/audio"; @@ -86,7 +86,7 @@ for (let i = 0; i < MAX_ENEMIES; i++) { const particles: Particle[] = []; for (let i = 0; i < MAX_PARTICLES; i++) { - particles.push({ x: 0, y: 0, vx: 0, vy: 0, life: 0, maxLife: 0, color: Color.White, active: false }); + particles.push({ x: 0, y: 0, vx: 0, vy: 0, life: 0, maxLife: 0, color: Colors.White, active: false }); } // Scrolling star background @@ -381,8 +381,8 @@ while (!windowShouldClose()) { } // HUD - drawText("SCORE: " + score.toString(), 10, 10, 20, Color.White); - drawText("WAVE: " + wave.toString(), SCREEN_WIDTH / 2 - 40, 10, 20, Color.White); + drawText("SCORE: " + score.toString(), 10, 10, 20, Colors.White); + drawText("WAVE: " + wave.toString(), SCREEN_WIDTH / 2 - 40, 10, 20, Colors.White); // Lives for (let i = 0; i < lives; i++) { @@ -397,16 +397,16 @@ while (!windowShouldClose()) { // Wave announcement if (waveTimer < 0) { const waveText = "WAVE " + wave.toString(); - drawText(waveText, SCREEN_WIDTH / 2 - measureText(waveText, 40) / 2, SCREEN_HEIGHT / 2 - 20, 40, Color.Yellow); + drawText(waveText, SCREEN_WIDTH / 2 - measureText(waveText, 40) / 2, SCREEN_HEIGHT / 2 - 20, 40, Colors.Yellow); } // Game over screen if (gameOver) { - drawText("GAME OVER", SCREEN_WIDTH / 2 - measureText("GAME OVER", 60) / 2, SCREEN_HEIGHT / 2 - 60, 60, Color.Red); + drawText("GAME OVER", SCREEN_WIDTH / 2 - measureText("GAME OVER", 60) / 2, SCREEN_HEIGHT / 2 - 60, 60, Colors.Red); const finalScore = "Score: " + score.toString(); - drawText(finalScore, SCREEN_WIDTH / 2 - measureText(finalScore, 30) / 2, SCREEN_HEIGHT / 2 + 10, 30, Color.White); + drawText(finalScore, SCREEN_WIDTH / 2 - measureText(finalScore, 30) / 2, SCREEN_HEIGHT / 2 + 10, 30, Colors.White); const restartText = "Press ENTER to restart"; - drawText(restartText, SCREEN_WIDTH / 2 - measureText(restartText, 20) / 2, SCREEN_HEIGHT / 2 + 60, 20, Color.LightGray); + drawText(restartText, SCREEN_WIDTH / 2 - measureText(restartText, 20) / 2, SCREEN_HEIGHT / 2 + 60, 20, Colors.LightGray); } endDrawing(); diff --git a/examples/voxel-sandbox/main.ts b/examples/voxel-sandbox/main.ts index bd612899..b7dbe83f 100644 --- a/examples/voxel-sandbox/main.ts +++ b/examples/voxel-sandbox/main.ts @@ -4,7 +4,7 @@ import { isMouseButtonPressed, closeWindow, beginMode3D, endMode3D, disableCursor, getMouseDeltaX, getMouseDeltaY, } from "bloom/core"; -import { Color, Key, Camera3D, MouseButton } from "bloom/core"; +import { Color, Colors, Key, Camera3D, MouseButton } from "bloom/core"; import { drawCube, drawCubeWires } from "bloom/models"; import { drawText } from "bloom/text"; import { drawRect } from "bloom/shapes"; @@ -284,15 +284,15 @@ function renderBlocks(): void { function drawHUD(): void { const cx = SCREEN_WIDTH / 2; const cy = SCREEN_HEIGHT / 2; - drawRect(cx - 10, cy - 1, 20, 2, Color.White); - drawRect(cx - 1, cy - 10, 2, 20, Color.White); + drawRect(cx - 10, cy - 1, 20, 2, Colors.White); + drawRect(cx - 1, cy - 10, 2, 20, Colors.White); const blockNames = ["", "Grass", "Dirt", "Stone", "Wood", "Leaves", "Sand", "Water"]; drawRect(5, SCREEN_HEIGHT - 35, 200, 30, { r: 0, g: 0, b: 0, a: 150 }); - drawText("Block: " + blockNames[selectedBlock] + " [1-7]", 10, SCREEN_HEIGHT - 30, 18, Color.White); + drawText("Block: " + blockNames[selectedBlock] + " [1-7]", 10, SCREEN_HEIGHT - 30, 18, Colors.White); drawText( "Pos: " + Math.floor(camX).toString() + ", " + Math.floor(camY).toString() + ", " + Math.floor(camZ).toString(), - 10, 10, 16, Color.White, + 10, 10, 16, Colors.White, ); } @@ -308,7 +308,7 @@ while (!windowShouldClose()) { if (highlightX >= 0) { drawCubeWires( { x: highlightX + 0.5, y: highlightY + 0.5, z: highlightZ + 0.5 }, - 1.02, 1.02, 1.02, Color.White, + 1.02, 1.02, 1.02, Colors.White, ); } endMode3D(); diff --git a/src/core/colors.ts b/src/core/colors.ts index a428207a..923de6aa 100644 --- a/src/core/colors.ts +++ b/src/core/colors.ts @@ -1,9 +1,7 @@ import { Color as ColorType } from './types'; -// Canonical color palette. Re-exported as `Color` from `bloom/core` and -// declared as a real top-level binding (not an alias re-export) so Perry -// emits a `_perry_fn_src_core_colors_ts__Color` symbol that examples -// importing `Color` from `bloom/core` can link against. +// Canonical color palette, exposed as `Colors` and `ColorConstants` from +// `bloom/core`. `Color` there is the RGBA type, not a runtime palette value. export const Color: Record = { Snow: { r: 245, g: 245, b: 245, a: 255 }, White: { r: 255, g: 255, b: 255, a: 255 }, diff --git a/tools/check-ci-contract.js b/tools/check-ci-contract.js index 57d3f489..335739bf 100755 --- a/tools/check-ci-contract.js +++ b/tools/check-ci-contract.js @@ -46,6 +46,16 @@ const testWorkflow = read(".github/workflows/test.yml"); const qualityWorkflow = read(".github/workflows/quality.yml"); const releaseWorkflow = read(".github/workflows/release.yml"); const failureAction = read(".github/actions/upload-ci-failure/action.yml"); +if (!/^defaults:\n run:\n(?: #[^\n]*\n)* shell: bash$/m.test(testWorkflow)) { + console.error("FAIL Tests must execute the shared Bash entry point with an explicit Bash shell on Windows"); + failures += 1; +} +for (const summary of ["target/ci/quick-shared-tests.json", "target/ci/full-host-build.json"]) { + if (!testWorkflow.includes(`path: ${summary}\n if-no-files-found: error`)) { + console.error(`FAIL Tests must reject missing execution evidence: ${summary}`); + failures += 1; + } +} const workflowCommands = [ "./scripts/ci-check.sh --quick --component shared-tests", "./scripts/ci-check.sh --quick --component contracts", From 636b69a19d8b2a50d63eee89a605b671ec8ad190 Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Fri, 11 Sep 2026 01:22:06 +0200 Subject: [PATCH 2/2] Preserve the MSVC linker ahead of Git Bash tools --- docs/evidence/windows-example-ci-v1.md | 8 ++++++++ scripts/ci-check.sh | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/docs/evidence/windows-example-ci-v1.md b/docs/evidence/windows-example-ci-v1.md index 4366cc21..186607d1 100644 --- a/docs/evidence/windows-example-ci-v1.md +++ b/docs/evidence/windows-example-ci-v1.md @@ -36,6 +36,14 @@ failure. The CI command contract checks both the shell and required evidence. Hosted execution on this correction still needs verification from actual Cargo output and summary contents. +The first hosted run at `88bc633` executes Cargo and uploads both failure +summaries. It exposes a second setup defect: Git Bash prepends its own +`/usr/bin/link.exe` ahead of the MSVC linker. Cargo then invokes the hard-link +utility with Microsoft linker arguments. The shared CI entry point now restores +the active Visual Studio developer toolchain ahead of Git's tools and fails +with an actionable message if that configured linker is missing. The next +hosted run must verify the build and shared suite with that correction. + Audit commands, original failure logs, compiler-release metadata, source patch, and the six corrected executable hashes are retained under `tools/quality/out/windows-engine-plan/all-examples/`. The original audit is also diff --git a/scripts/ci-check.sh b/scripts/ci-check.sh index c12791a7..65cc3870 100755 --- a/scripts/ci-check.sh +++ b/scripts/ci-check.sh @@ -200,6 +200,20 @@ on_exit() { } trap on_exit EXIT +# Git Bash prepends /usr/bin after the MSVC setup action has populated PATH. +# Its `link.exe` creates hard links; rustc must use the Microsoft linker. +# Restore the active developer toolchain first for Cargo and C/C++ build scripts. +if [ "$host_crate" = "windows" ] && [ -n "${VCToolsInstallDir:-}" ]; then + msvc_bin="$(cygpath -u "$VCToolsInstallDir")/bin/Host${VSCMD_ARG_HOST_ARCH:-x64}/${VSCMD_ARG_TGT_ARCH:-x64}" + if [ ! -f "$msvc_bin/link.exe" ]; then + echo "MSVC linker missing: $msvc_bin/link.exe; initialize the Visual Studio C++ developer environment" >&2 + exit 2 + fi + export PATH="$msvc_bin:$PATH" + hash -r + hr "Windows linker: $(command -v link.exe)" +fi + run_component() { CURRENT_COMPONENT="$1" case "$CURRENT_COMPONENT" in