Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,8 @@ jobs:
~/.cargo/registry
~/.cargo/git
native/windows/target
key: ${{ runner.os }}-windows-crate-${{ hashFiles('native/windows/Cargo.lock') }}
native/web/target
key: ${{ runner.os }}-windows-crate-web-${{ hashFiles('native/windows/Cargo.lock', 'native/web/Cargo.lock') }}
restore-keys: ${{ runner.os }}-windows-crate-

- name: full / host-build
Expand Down Expand Up @@ -338,6 +339,29 @@ jobs:
if-no-files-found: error
retention-days: 1

- name: Prepare pinned Windows web build tool
shell: pwsh
run: |
python tools/ci/setup_windows_wasm_pack.py --out "$env:RUNNER_TEMP/bloom-wasm-pack" --github-env
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
rustup target add wasm32-unknown-unknown

- name: Windows / installed starter web run
shell: pwsh
env:
CARGO_BUILD_JOBS: '2'
run: python tools/ci/starter_package_smoke.py --web --out target/ci/starter-web

- name: Retain complete installed starter website
if: always()
uses: actions/upload-artifact@v4
with:
name: bloom-starter-web-${{ github.run_id }}
path: target/ci/starter-web
include-hidden-files: true
if-no-files-found: error
retention-days: 1

- name: Windows / installed native startup
shell: pwsh
env:
Expand Down Expand Up @@ -523,6 +547,12 @@ jobs:
name: bloom-compiled-web-game-${{ github.run_id }}
path: target/ci/compiled-web-game

- name: Download installed starter website
uses: actions/download-artifact@v4
with:
name: bloom-starter-web-${{ github.run_id }}
path: target/ci/starter-web

- name: web / browser-smoke
run: ./scripts/ci-check.sh --web --component browser-smoke

Expand Down
95 changes: 95 additions & 0 deletions docs/evidence/installed-starter-browser-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Complete installed starter browser acceptance

The minimal compiled-game fixture verifies rendering and lifecycle hooks, but
does not exercise the generated starter's asset read, named function callbacks
or text rendering. The new gate creates a project through the installed CLI,
then runs the installed `bloom run --web` command against its unchanged source.
The command compiles game and engine WASM, writes the asset manifest and starts
its development server. The preparer checks served HTML, manifest, asset and
engine bytes, including the WASM MIME type, then stops its owned process tree.
The complete generated website and every file's SHA-256 are retained.

Windows CI uses the existing qualified Perry 0.5.1220 profile and a pinned
[official wasm-pack 0.15.0 archive](https://github.com/wasm-bindgen/wasm-pack/releases/tag/v0.15.0).
Archive SHA-256 is checked before selecting the executable from the tar file;
the tool's version and executable hash are recorded. The build adds no test
markers or alternate logic to the starter source.

Hosted macOS Chrome loads this exact website through its production bootstrap.
The monitor observes actual asset fetch/read, text and rectangle FFI calls and
the real Perry callback dispatcher. It also verifies every clear's opaque-black
RGBA arguments. It requests normal engine stop after eight
successful frames and requires exactly one cleanup. The screenshot must contain
the 800x450 viewport, the square's white interior at its actual interpolated
position, a populated text region and the expected black background. Text
coverage allows glyph antialiasing differences; the real draw call must contain
the exact text read from `assets/welcome.txt`. The existing startup-fault gate
remains required. This new gate does not replace the game's draw calls or load
a fake physics factory.

## Failure found by executing the full starter

The first real generated WASM run fails during named `init` callback dispatch:
Perry exports that void function without a WASM result, but its closure bridge
passes the returned JavaScript `undefined` into an i64 decoder. The decoder then
raises `Cannot mix BigInt and other types`. Native starter rendering passed;
compilation-only web evidence did not expose this startup failure. The raw
failed probe and the observed `undefined` return are retained.

Bloom's production splicer now installs a compatibility bridge that preserves
an actual void return and delegates every boxed value to the existing decoder.
It does not suppress other decoder exceptions. Actual native/WASM lifecycle
checks now cover named functions for all five hooks. A recording-FFI probe runs
the real compiled starter and production scheduler, reads the asset text,
records eight text/rectangle calls and cleans up once with the correction.
That initial probe did not validate color channels, load the engine renderer
or claim browser/network acceptance.

The corrected installed `bloom run --web` command passes locally in 64.531
seconds, including game/engine compilation, serving the four required resources
with exact file bytes and the correct WASM MIME type, and controlled shutdown.
The resulting 11-file website includes the actual production compatibility
bridge. Running that exact generated boot script with the recording FFI also
passes all starter callbacks and one cleanup. Ten Python acceptance-control
tests and the repository contracts pass; the text/image verifier accepts the
previous actual native starter capture. Hosted browser rendering is pending.

The first hosted run, 34571662102, passed all 21 preceding Tests jobs and the
existing browser checks. The complete starter verifier then rejected the
downloaded website before launch: upload-artifact omitted wasm-pack's
`pkg/.gitignore`, while its build receipt correctly included all 11 files.
All ten downloaded file hashes match. The scoped starter artifact upload now
includes hidden files, preserving the complete site and the exact hash check.
That failure remains retained.

The retry, 34574961654, passed those same 21 preceding jobs and both existing
browser checks. The complete site passed its file-hash check, loaded the actual
asset and reached drawing. The full starter then failed its first frame:
`Colors.BLACK` and `Colors.WHITE` reached FFI with undefined RGBA channels, and
WebGPU rejected the non-finite clear alpha. Its error, draw arguments and
failure screenshot are retained. This demonstrates why call counts and a
successful build are insufficient evidence of complete starter execution.

The starter now defines its two colors as explicit typed RGBA literals, matching
the qualified native examples. Its strengthened actual-WASM recording probe
rejects the earlier site and accepts the fresh build's exact black clear and
white text/square arguments in all eight frames, with one cleanup. The hosted
monitor now retains clear arguments as well as text/rectangle arguments; the
verifier rejects missing channels. Fresh installed `bloom run --web` passes in
47.218 seconds with unchanged source, the complete website and exact served
bytes retained. This correction qualifies the starter's color usage; general
imported palette compatibility remains separate. Hosted rendering of the
corrected starter remains pending.

The corrected unmodified starter also compiles and links natively. A bounded
copy on Radeon 760M / DX12 renders the expected 800x450 black background, white
square and asset text (857 lit text pixels), exits after nine frames and runs
cleanup once. The capture SHA-256 is
`d89c3b30d0e472526d6a39869e0c9e3ae3e9c0943f87b2dbf87a34cc91ac0b3f`.
This local native check uses a linked checkout and the SDK's shader dependencies;
it does not qualify a clean Windows installation.

General Perry exception propagation, browser physics acceptance, the complete
canonical runtime matrix, visible native presentation and clean distribution
packaging remain separate. A browser warning about optional physics startup is
retained as observed; the starter itself does not exercise physics.
6 changes: 6 additions & 0 deletions docs/starter.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,9 @@ The fixed-lifecycle follow-up repeats fresh installed default creation, native
build and the bounded greeting/asset/cleanup run using the revised template,
then completes its full web build. See [the lifecycle evidence](evidence/fixed-game-lifecycle-v1.md).
The browser runtime still needs to qualify this complete starter.

The [complete starter browser gate](evidence/installed-starter-browser-v1.md)
now runs the installed web command and retains its unchanged generated website
for hosted rendering, including the actual asset read, text draw and cleanup.
It also covers the production bridge for named void callbacks in Perry WASM.
Its hosted result is pending.
7 changes: 6 additions & 1 deletion docs/windows-engine-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ first nine-scene Radeon evidence are in draft PR #154. Follow-up work starts at
| #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 | #160 fixes silent Windows CI non-execution and MSVC PATH ordering. #161 passes the actual native engine build and all 20 native links locally and in hosted CI. #162 fixes the focused DX12 failures; #163 fixes camera-history reset. #164 passes all 22 hosted Tests jobs using an explicit FXC Windows lane. The underlying WARP/DXIL crash remains open. A separate layered-material correction passes the full local FXC shared suite and all 93 DXC/Vulkan goldens. Fresh installed headless scene/direct-2D rendering and cleanup pass through #169. Visible presentation and release packaging remain open. #170's initial Windows shared job again crashes despite FXC; the serial follow-up and #171 each pass all 22 hosted Tests jobs. The driver root cause remains open |
| #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–#171 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–#172 remain drafts; no merge performed |

## Engine work retained in scope

Expand Down Expand Up @@ -61,6 +61,11 @@ audit are saved in `tools/quality/out/windows-engine-plan/plan-requirements.json
pass default project creation and setup-error controls in hosted CI. The full
starter's assets/text browser rendering and canonical example runtime matrix
remain open. All 20 native examples compile and link.
The [complete starter browser candidate](evidence/installed-starter-browser-v1.md)
now runs the installed web command, verifies its served files and retains the
unchanged site for hosted asset/text/render acceptance. Executing its real WASM
found a named-void-callback return conversion failure; the production bootstrap
bridge and expanded named-hook contracts pass locally. Hosted rendering is pending.
2. **Finish and qualify fixed lifecycle integration.**
The [fixed lifecycle candidate](evidence/fixed-game-lifecycle-v1.md) passes pure
native/WASM timing and hook-order contracts, plus exact installed rendering
Expand Down
17 changes: 15 additions & 2 deletions native/web/splice_game.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ const BLOOM_SHELL = `
<script type="module" src="./bloom_glue.js"></script>
`;

function installPerryVoidReturnCompatibility(runtime) {
// Perry 0.5.1220 exports named void functions with no WASM result, while
// its closure bridge always decodes the return as a NaN-boxed i64. Preserve
// an actual void result; keep the original decoder for every boxed value.
const decode = runtime.__bitsToJsValue;
if (typeof decode !== "function") {
throw new Error("Perry runtime has no value decoder; use the supported compiler version.");
}
runtime.__bitsToJsValue = function(bits) {
return bits === undefined ? undefined : decode(bits);
};
}

function splice(html) {
if (!html.includes(PERRY_ROOT)) {
throw new Error("could not find perry-root in Perry HTML; compiler output format may have changed");
Expand All @@ -43,10 +56,10 @@ function splice(html) {
}
tail = tail.replace(BOOT_CALL, 'window.__bloomReady.then(() => bootPerryWasm("');
tail = tail.replace(BOOT_CATCH, '\")).catch(');
return head + tail;
return head + `(${installPerryVoidReturnCompatibility.toString()})(globalThis);\n` + tail;
}

module.exports = { splice };
module.exports = { splice, installPerryVoidReturnCompatibility };
if (require.main === module) {
try {
if (process.argv.length !== 4) throw new Error("Usage: splice_game.cjs <perry_html_in> <output_html_out>");
Expand Down
7 changes: 7 additions & 0 deletions scripts/ci-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,11 @@ run_component() {
tools/ci/test_compiled_web_smoke.py \
tools/ci/fixed_step_smoke.py \
tools/ci/test_fixed_step_smoke.py \
tools/ci/starter_package_smoke.py \
tools/ci/starter_web_run.py \
tools/ci/starter_browser_smoke.py \
tools/ci/test_starter_browser_smoke.py \
tools/ci/setup_windows_wasm_pack.py \
tools/ci/test_compile_examples.py
"$python_cmd" -m unittest \
tools/quality/test_run.py \
Expand All @@ -303,6 +308,7 @@ run_component() {
tools/ci/test_compile_examples.py \
tools/ci/test_compiled_web_smoke.py \
tools/ci/test_fixed_step_smoke.py \
tools/ci/test_starter_browser_smoke.py \
-v
hr "visual metric and fault-engine tests"
cargo test --release --manifest-path tools/bloom-diff/Cargo.toml
Expand Down Expand Up @@ -340,6 +346,7 @@ run_component() {
"$python_cmd" tools/ci/web_smoke.py
hr "Perry compiled-game startup, frame, cleanup and failure control"
"$python_cmd" tools/ci/compiled_web_smoke.py
"$python_cmd" tools/ci/starter_browser_smoke.py
;;
target-check)
cross_crate="${BLOOM_CROSS_CRATE:-}"
Expand Down
4 changes: 3 additions & 1 deletion tools/ci/fixed_step_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"overflowRejected": True, "hugeAlpha": 0.9, "tinyBounded": True,
"finiteContract": True, "sparseStarted": True, "sparseDraws": 1,
"updateStopEvents": "UC", "invalidDriverRejected": True, "invalidCalls": 0,
"namedEvents": "IFUDC",
}


Expand Down Expand Up @@ -95,7 +96,8 @@ def run(name, command):
report["source_sha256"] = {
name: hashlib.sha256((ROOT / name).read_bytes()).hexdigest()
for name in ("src/core/fixed_step.ts", "src/core/game_lifecycle.ts", "src/core/numbers.ts",
"tools/ci/fixtures/fixed-step.ts", "tools/ci/perry_wasm_console.cjs")
"tools/ci/fixtures/fixed-step.ts", "tools/ci/perry_wasm_console.cjs",
"native/web/splice_game.cjs")
}
for mode in ("native", "wasm"):
artifact = out / ("fixed-step.html" if mode == "wasm" else "fixed-step.exe" if os.name == "nt" else "fixed-step")
Expand Down
13 changes: 13 additions & 0 deletions tools/ci/fixtures/fixed-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@ const invalidDriverRejected = !invalidDriver.initialize();
invalidDriver.frame(0.01, () => false);
invalidDriver.dispose();

let namedEvents = '';
function namedInit(): void { namedEvents = namedEvents + 'I'; }
function namedFixed(_dt: number, _tick: number): void { namedEvents = namedEvents + 'F'; }
function namedUpdate(_dt: number): void { namedEvents = namedEvents + 'U'; }
function namedDraw(_alpha: number): void { namedEvents = namedEvents + 'D'; }
function namedCleanup(): void { namedEvents = namedEvents + 'C'; }
const namedDriver = new GameLifecycleDriver({ init: namedInit, fixedUpdate: namedFixed,
update: namedUpdate, draw: namedDraw, cleanup: namedCleanup });
namedDriver.initialize();
namedDriver.frame(1 / 60, () => false);
namedDriver.dispose();

// Read each observation directly. The original object-literal JSON.stringify
// report returned undefined in Perry WASM; preserve that separately from the
// timing/lifecycle contract. Events contain only this fixture's fixed ASCII tags.
Expand Down Expand Up @@ -142,4 +154,5 @@ result = result + ",\"sparseDraws\":" + sparseDraws;
result = result + ",\"updateStopEvents\":\"" + updateStopEvents + "\"";
result = result + ",\"invalidDriverRejected\":" + invalidDriverRejected;
result = result + ",\"invalidCalls\":" + invalidCalls;
result = result + ",\"namedEvents\":\"" + namedEvents + "\"";
console.log("BLOOM_FIXED_STEP_RESULT:" + result + "}");
2 changes: 2 additions & 0 deletions tools/ci/perry_wasm_console.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// renderer, GPU, network or FFI stubs participate in the contract result.
const fs = require("node:fs");
const vm = require("node:vm");
const { installPerryVoidReturnCompatibility } = require("../../native/web/splice_game.cjs");

async function main() {
if (process.argv.length !== 3) throw new Error("Usage: perry_wasm_console.cjs <compiled-fixture.html>");
Expand Down Expand Up @@ -36,6 +37,7 @@ async function main() {
context.window = context;
context.self = context;
vm.runInContext(scripts[0][1], context, { timeout: 5000 });
vm.runInContext(`(${installPerryVoidReturnCompatibility.toString()})(globalThis);`, context, { timeout: 5000 });
const boot = context.bootPerryWasm;
if (typeof boot !== "function") throw new Error("Perry runtime has no boot entry");
let completion;
Expand Down
53 changes: 53 additions & 0 deletions tools/ci/setup_windows_wasm_pack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Prepare the pinned official Windows wasm-pack binary for the starter gate."""

import argparse
import hashlib
import io
import json
import os
from pathlib import Path
import subprocess
import tarfile
import urllib.request

VERSION = '0.15.0'
ARCHIVE = f'wasm-pack-v{VERSION}-x86_64-pc-windows-msvc.tar.gz'
URL = f'https://github.com/wasm-bindgen/wasm-pack/releases/download/v{VERSION}/{ARCHIVE}'
SHA256 = '518dc51180c7bc864699c9279b3bc99025bc109123e0249a34ba34d130a509bf'


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--out', type=Path, required=True)
parser.add_argument('--github-env', action='store_true')
args = parser.parse_args()
out = args.out.resolve()
out.mkdir(parents=True, exist_ok=True)
archive = out / ARCHIVE
if not archive.exists() or hashlib.sha256(archive.read_bytes()).hexdigest() != SHA256:
with urllib.request.urlopen(URL, timeout=120) as response:
data = response.read()
if hashlib.sha256(data).hexdigest() != SHA256:
raise RuntimeError('official wasm-pack archive SHA-256 differs from the pinned release')
archive.write_bytes(data)
with tarfile.open(fileobj=io.BytesIO(archive.read_bytes()), mode='r:gz') as tar:
files = [member for member in tar.getmembers() if member.isfile() and Path(member.name).name == 'wasm-pack.exe']
if len(files) != 1:
raise RuntimeError('official wasm-pack archive must contain exactly one executable')
data = tar.extractfile(files[0]).read()
executable = out / 'wasm-pack.exe'
executable.write_bytes(data)
version = subprocess.check_output([str(executable), '--version'], text=True).strip()
if version != f'wasm-pack {VERSION}':
raise RuntimeError(f'unexpected wasm-pack version: {version}')
receipt = dict(url=URL, archive_sha256=SHA256, executable_sha256=hashlib.sha256(data).hexdigest(), version=version)
(out / 'toolchain.json').write_text(json.dumps(receipt, indent=2) + '\n', encoding='utf-8')
if args.github_env:
with open(os.environ['GITHUB_ENV'], 'a', encoding='utf-8') as output:
output.write(f'BLOOM_WASM_PACK={executable}\n')
print(json.dumps(receipt))


if __name__ == '__main__':
main()
Loading
Loading