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
12 changes: 12 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,18 @@ jobs:
# MSVC is installed on the runner.
uses: ilammy/msvc-dev-cmd@v1

- name: Windows / installed starter creation
shell: pwsh
run: python tools/ci/starter_package_smoke.py

- name: Retain installed starter evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: windows-installed-starter
path: target/ci/installed-starter
if-no-files-found: error

- name: Cache cargo
uses: actions/cache@v4
with:
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ cd dist/web && python3 -m http.server 8080

## Features

The draft [starter command](docs/starter.md) adds project creation, native
build/run and web build/serve from one source. It includes the template,
permissions and asset example. Use the documented draft package flow; these
commands are not present in stable npm 0.4.16.

- **Simple API** — A function-first gameplay API with plain data handles. ([design rationale](docs/design-api.md))
- **True native** — Compiles to Metal, DirectX 12, Vulkan, OpenGL, and WebGPU via wgpu.
- **Ship everywhere** — macOS, Windows, Linux, iOS, tvOS, watchOS, visionOS, Android, and Web from one codebase.
Expand Down
55 changes: 55 additions & 0 deletions docs/evidence/windows-starter-cli-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Installed starter command acceptance

The new package includes `bloom new/run/build`, a shared TypeScript template,
asset example and local web server. The generator carries the exact engine
archive into each project, avoiding a preview/stable collision at the existing
0.4.16 version number. Native build/run selects the host platform; web uses the
existing actual Perry-plus-engine builder and writes a prefetch manifest.

## Local execution

The first fresh-package probe uses the explicit local archive option. Packing,
bootstrap install, installed command help and installed project creation all
pass. The unmodified starter builds natively in 270.360 seconds. The installed
`npm start` path then builds/runs a bounded copy in 10.141 seconds. Capture/state
instrumentation preserves the template's init/update/draw/cleanup logic.

The 800x450 capture shows the greeting loaded from `assets/welcome.txt` and the
moving white square on black. Checks require its observed square center,
background corners, at least 4,096 white pixels and exactly one cleanup. Visual
inspection also confirms the greeting. The PNG SHA-256 is
`be7a32be2fb93fb24fa838370dc9c656c7b363ba19fe9b12f1681aa4b9d1acbd`.
These checks are not a claim that all 360,000 pixels are an exact golden.

The probe restores the unmodified source and completes the full web build in
63.765 seconds. The generated asset manifest includes `assets/welcome.txt`,
whose bytes match the source. Engine WASM SHA-256 is
`c81e7d5760d7fc4de30b14f8a177f71b1a0988669252eab7e7e1aaedc7342d65`.
No compiled-starter browser rendering is claimed by this build result.

The local native run uses the qualified Perry 0.5.1220 source/runtime profile,
Radeon 760M DX12, headless pixel-exact output and SDK shader DLLs on PATH. It
installs Jolt from the package dependency without repository prebuilt overrides.
Executables and the installed diagnostic project remain private.

## Creation and failure controls

The default creation follow-up packs the running installed engine instead of
selecting an older registry package with the same version. The installed-package
checker verifies the generated source, packaged command/setup files, asset and
archive reference, and requires nonzero actionable failures for a missing
compiler and unsupported target. This local default-creation check passes.
The cached managed Windows profile also passes compiler/library hash validation
and selects its matching runtime environment. It does not exercise a fresh
toolchain download. Hosted acceptance remains pending.

Five unit checks cover help without tools, argument rejection, manifest drift,
existing-project preservation, explicit archive copying, compiler/process
failure causes and local serving. Repository contracts pass. Local HTTP checks
request an owned test server's generated files; they do not control a browser.

Visible native startup, actual compiled-starter browser rendering, macOS/Linux
starter runtime, automatic fresh-machine toolchain setup through this CLI,
all-example runtime coverage, fixed updates, hot reload and distributable
shader-runtime packaging remain open. The tested native CLI uses an explicitly
prepared toolchain. No npm release or PR merge is performed.
78 changes: 78 additions & 0 deletions docs/starter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Starter commands

This draft adds `bloom new`, `bloom run`, `bloom run --web` and `bloom build`.
They are not available in the stable npm 0.4.16 package. To try the draft from
its checkout, first create a package archive:

```sh
npm pack --ignore-scripts
npm exec --package ./bloomengine-engine-0.4.16.tgz -- bloom new my-game
cd my-game
npm start
```

The creation command installs dependencies and writes one TypeScript source,
an asset example, Bloom/Perry version metadata and the native-library permissions.
It packs the exact engine package running the command into `.bloom/engine.tgz`,
which the project references locally. This keeps preview and registry packages
with the same version number from being confused. Keep the archive and generated
lockfile with the project. `--engine-package <local.tgz>` supplies a specific
compatible archive; `--no-install` defers dependency installation.

## Prerequisites and targets

- Node.js 18+ with npm, plus Rust.
- Windows native: Python 3.12+, Git and Visual Studio C++ build tools. Use a
developer shell. The first build prepares Perry 0.5.1220 and matching source
libraries under `%LOCALAPPDATA%/Bloom/perry-0.5.1220`. The existing verified
setup helper handles the compiler download and Cargo commands. Later builds
verify and reuse that cache.
- macOS/Linux native: Perry 0.5.1220 and the host platform SDK. These starter
command paths require their own runtime qualification; local evidence is Windows.
- Web: Perry 0.5.1220, wasm-pack and a WebGPU-capable browser. Set BLOOM_PERRY to
the executable prepared by Windows if that compiler is not on PATH.

```sh
npm run web
npm exec -- bloom run --web --port 8081
npm exec -- bloom build --target web
npm run build
```

Web run builds and serves at `http://127.0.0.1:8080` until Ctrl+C. It does not
automatically open a browser. Both native and web build the same `main.ts`.
The web asset manifest is generated before serving, so the glue prefetches the
template's text asset before synchronous game initialization. Native run uses
the distribution directory as its working directory to find the copied assets.

Build targets are `native`, `web`, `windows`, `linux` and `macos`; native targets
must match the current host. Mobile deployment remains in its platform SDK flow.
Release builds omit Perry's debug-symbol flag. Signing, installers, DXC/DXIL
distribution and publishing are separate packaging work.

The Windows toolchain location can be changed with BLOOM_TOOLCHAIN_DIR. To use
an explicitly prepared native profile, set BLOOM_PERRY, PERRY_RUNTIME_DIR and
PERRY_WORKSPACE_ROOT together. BLOOM_PYTHON controls the setup interpreter.
Perry currently requires crate-local native Cargo output; the command rejects
a configured CARGO_TARGET_DIR instead of reporting a successful build with no
executable. General long Windows project paths remain unqualified.

## Development and validation

The template separates init, variable update, draw and cleanup while using
`runGame` on both targets. See the [game-loop contract](game-loop.md). Edit,
stop and rerun to rebuild; automatic hot reload, fixed update and device-loss
recovery are not implemented by this command.

Local installed-package acceptance creates the project through the installed
npm command, builds the unmodified template natively, and completes the full
web build from that same source. A bounded copy adds capture/state/cleanup
observations for native execution. It renders the 800x450 greeting and square,
loads `assets/welcome.txt` and cleans up once on Radeon DX12. This does not prove
visible presentation, compiled-starter browser rendering or a clean machine's
shader DLL packaging.

The installed-package CI check verifies default creation from the exact packed
engine and rejects missing compilers and unsupported targets. Unit tests cover
manifest drift, existing-project preservation, asset inventory and the local
server. See [the retained acceptance report](evidence/windows-starter-cli-v1.md).
7 changes: 5 additions & 2 deletions docs/windows-engine-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,11 @@ audit are saved in `tools/quality/out/windows-engine-plan/plan-requirements.json
exact scene/direct-2D frames and simulate Jolt locally and in hosted CI.
[Shared cleanup, corrected example palettes and Pong pause replay](evidence/windows-game-cleanup-v1.md)
are [published at #169](https://github.com/Bloom-Engine/engine/releases/tag/quality-evidence-starter-lifecycle-20260911).
All 20 canonical native examples link. Project creation/build/run commands,
all-example web/runtime acceptance, fixed updates, visible native presentation,
All 20 canonical native examples link. The [starter command](starter.md)
creates a project from an installed package; its unmodified native/web builds
and bounded native greeting/asset/cleanup run pass locally. Default creation
now carries the exact engine archive, with hosted packaging checks pending.
All-example web/runtime acceptance, fixed updates, visible native presentation,
packaged DXC/DXIL and general Windows long-path support remain incomplete.
4. **Complete wider graphics and performance acceptance.**
[Two strict full Radeon runs at #159](https://github.com/Bloom-Engine/engine/releases/tag/quality-evidence-ssgi-surface-20260911)
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"main": "src/index.ts",
"types": "src/index.ts",
"bin": {
"bloom": "tools/cli/bloom.cjs",
"bloom-web": "native/web/build.cjs"
},
"exports": {
Expand All @@ -24,6 +25,8 @@
"./quality": "./src/quality/index.ts"
},
"files": [
"tools/cli/**",
"tools/ci/setup_windows_perry.py",
"src/",
"perry.config.ts",
"native/shared/Cargo.toml",
Expand Down
1 change: 1 addition & 0 deletions scripts/ci-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ run_component() {
contracts)
hr "CI command inventory"
node tools/check-ci-contract.js
node --test tools/ci/test_starter_cli.cjs
hr "FFI/schema parity"
node tools/validate-ffi.js
hr "documentation and package contracts"
Expand Down
100 changes: 100 additions & 0 deletions tools/ci/starter_package_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Require the packed starter CLI to create a project with the exact engine package."""

import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import time

ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from tools.ci.native_package_smoke import npm_command


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--out", type=Path, default=ROOT / "target/ci/installed-starter")
args = parser.parse_args()
out = args.out.resolve()
out.mkdir(parents=True, exist_ok=True)
report = {"schema": "bloom-installed-starter-v1", "status": "running", "commands": []}
def save():
(out / "result.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
def run(name, command, cwd, env=None, expected_error=None):
record = {"name": name, "command": command, "cwd": str(cwd)}
report["commands"].append(record)
save()
start = time.monotonic()
with (out / f"{name}.stdout.log").open("wb") as stdout, (out / f"{name}.stderr.log").open("wb") as stderr:
result = subprocess.run(command, cwd=cwd, env=env, stdout=stdout, stderr=stderr, timeout=240)
record.update(exit_code=result.returncode, duration_seconds=round(time.monotonic() - start, 3))
save()
stdout = (out / f"{name}.stdout.log").read_text(encoding="utf-8", errors="replace")
stderr = (out / f"{name}.stderr.log").read_text(encoding="utf-8", errors="replace")
if expected_error:
if result.returncode == 0 or expected_error not in stdout + stderr:
raise RuntimeError(f"{name}: missing expected nonzero failure: {expected_error}")
elif result.returncode:
raise RuntimeError(f"{name} exited {result.returncode}; see retained logs")
return stdout
parent = Path(tempfile.gettempdir()).resolve()
temporary = Path(tempfile.mkdtemp(prefix="bsc-", dir=parent)).resolve()
save()
try:
npm = npm_command()
report["source_commit"] = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip()
report["source_dirty"] = bool(subprocess.check_output(["git", "status", "--porcelain"], cwd=ROOT))
packed = json.loads(run("pack", npm + ["pack", "--json", "--ignore-scripts", "--pack-destination", str(temporary)], ROOT))[0]
archive = temporary / packed["filename"]
report["package_sha256"] = hashlib.sha256(archive.read_bytes()).hexdigest()
bootstrap = temporary / "bootstrap"
bootstrap.mkdir()
(bootstrap / "package.json").write_text('{"name":"starter-cli-bootstrap","private":true}\n')
run("install", npm + ["install", "--ignore-scripts", "--no-audit", "--no-fund", str(archive)], bootstrap)
run("help", npm + ["exec", "--", "bloom", "--help"], bootstrap)
project = temporary / "game"
run("create", npm + ["exec", "--", "bloom", "new", str(project)], bootstrap)
manifest = json.loads((project / "package.json").read_text())
if manifest["dependencies"]["@bloomengine/engine"] != "file:.bloom/engine.tgz":
raise RuntimeError("generated project must pin the exact engine archive")
installed = project / "node_modules/@bloomengine/engine"
source_files = ["tools/cli/bloom.cjs", "tools/cli/toolchain.cjs", "tools/cli/serve.cjs",
"tools/cli/templates/main.ts", "tools/cli/templates/README.md",
"tools/ci/setup_windows_perry.py"]
report["installed_source_sha256"] = {}
for name in source_files:
# npm normalizes executable shebang line endings on Windows.
if (installed / name).read_text(encoding="utf-8") != (ROOT / name).read_text(encoding="utf-8"):
raise RuntimeError(f"installed starter source differs: {name}")
report["installed_source_sha256"][name] = hashlib.sha256((installed / name).read_bytes()).hexdigest()
if (project / "main.ts").read_text(encoding="utf-8") != (ROOT / "tools/cli/templates/main.ts").read_text(encoding="utf-8"):
raise RuntimeError("generated game source differs from the shipped template")
if (project / "assets/welcome.txt").read_text().strip() != "Hello, Bloom!":
raise RuntimeError("starter asset is missing")
report["project_engine_archive_sha256"] = hashlib.sha256((project / ".bloom/engine.tgz").read_bytes()).hexdigest()
report["project_config"] = json.loads((project / "bloom.json").read_text())
run("unsupported-target", npm + ["exec", "--", "bloom", "build", "--target", "android"], project, expected_error="Unsupported starter target")
missing_env = {**os.environ, "BLOOM_PERRY": str(temporary / "missing-perry.exe")}
run("missing-compiler", npm + ["exec", "--", "bloom", "build", "--target", "web"], project, env=missing_env, expected_error="not found")
report["status"] = "pass"
print("PASS: installed CLI creates an exact-package starter; setup failure controls rejected")
return 0
except (OSError, ValueError, RuntimeError, KeyError, subprocess.SubprocessError) as error:
report.update(status="fail", error=str(error))
print(f"FAIL: {error}")
return 1
finally:
save()
if temporary.parent != parent or temporary.is_symlink() or temporary.is_junction():
raise RuntimeError(f"refusing cleanup outside owned temporary directory: {temporary}")
shutil.rmtree(temporary)


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading