From a624ccded0a050fddc24ccfbb8079dfb8032805c Mon Sep 17 00:00:00 2001 From: Vladislav Prusakov Date: Wed, 4 Nov 2026 14:25:12 +0300 Subject: [PATCH] Move all technical demos to standalone AdaExamples packages --- .../references/architecture.md | 2 +- Demos/A2UIForm/A2UIFormDemo.swift | 199 ---------- Demos/A2UIForm/Fixtures/form.jsonl | 5 - Demos/A2UIForm/README.md | 8 - Demos/A2UIForm/script/build_and_run.sh | 69 ---- Demos/AndroidDemo/AndroidDemo.swift | 39 -- Demos/AndroidDemo/Assets/fixture.txt | 1 - Demos/AndroidDemo/README.md | 5 - Demos/LinuxSmoke/LinuxSmoke.swift | 49 --- Demos/MovieMaker/MovieMakerDemo.swift | 75 ---- Demos/Navigation/NavigationDemoApp.swift | 201 ---------- Demos/Navigation/NavigationDemoFixture.swift | 115 ------ Demos/Navigation/NavigationDemoMain.swift | 148 ------- Demos/Navigation/NavigationDemoScript.swift | 24 -- Demos/Navigation/NavigationRobot.ada | 20 - .../.codex/environments/environment.toml | 8 - Demos/Physics2D/.gitignore | 1 - Demos/Physics2D/Physics2DAdvancedChecks.swift | 81 ---- Demos/Physics2D/Physics2DAdvancedDemo.swift | 202 ---------- Demos/Physics2D/Physics2DCharactersDemo.swift | 93 ----- Demos/Physics2D/Physics2DDemoMain.swift | 364 ----------------- Demos/Physics2D/Physics2DDemoScenes.swift | 302 -------------- Demos/Physics2D/Physics2DDemoView.swift | 70 ---- .../Physics2D/Physics2DLargeWorldChecks.swift | 86 ---- Demos/Physics2D/Physics2DLargeWorldDemo.swift | 152 ------- Demos/Physics2D/Physics2DMechanismsDemo.swift | 132 ------- Demos/Physics2D/Physics2DQueriesDemo.swift | 135 ------- Demos/Physics2D/README.md | 61 --- Demos/Physics2D/script/build_and_run.sh | 68 ---- Demos/SpineNative/Package.resolved | 150 ------- Demos/SpineNative/Package.swift | 27 -- Demos/SpineNative/README.md | 41 -- Demos/SpineNative/SpineNativeApp.swift | 141 ------- Demos/SpineNative/capture.py | 85 ---- .../Assets/SpineRuntime-LICENSE.txt | 26 -- Demos/SpinePreview/Assets/UPSTREAM.md | 5 - Demos/SpinePreview/Assets/preview.html | 75 ---- Demos/SpinePreview/Assets/spineboy-pro.skel | Bin 64463 -> 0 bytes Demos/SpinePreview/Assets/spineboy.atlas | 94 ----- Demos/SpinePreview/Assets/spineboy.png | Bin 245321 -> 0 bytes Demos/SpinePreview/README.md | 23 -- Demos/SpinePreview/SpinePreview.swift | 80 ---- Demos/Sponza/Camera/SponzaCameraMotion.swift | 30 -- Demos/Sponza/Camera/SponzaFlyCamera.swift | 48 --- Demos/Sponza/Camera/SponzaLightMotion.swift | 13 - Demos/Sponza/Package.resolved | 140 ------- Demos/Sponza/Package.swift | 19 - Demos/Sponza/README.md | 178 --------- Demos/Sponza/SponzaDemo.swift | 370 ------------------ Demos/Sponza/SponzaDemoProfiler.swift | 357 ----------------- Demos/Sponza/SponzaLighting.swift | 134 ------- Demos/Sponza/SponzaStutterProfiler.swift | 72 ---- Demos/Sponza/SponzaWarmup.swift | 111 ------ Demos/Sponza/Tests/SponzaCameraTests.swift | 66 ---- Demos/SpriteInstancingValidation/README.md | 39 -- .../SpriteValidationApp.swift | 152 ------- .../SpriteValidationFixture.swift | 154 -------- Demos/SpriteInstancingValidation/project.yml | 44 --- Documentation/A2UI.md | 4 +- Documentation/Linux.md | 3 +- .../SponzaLightingPerformance-2026-11-03.md | 2 +- Documentation/Physics2D.md | 2 +- Documentation/Physics2DAdvanced.md | 4 +- Documentation/Spine2D.md | 4 +- Documentation/SpriteInstancing.md | 2 +- .../Assets/Documentation/catalog.json | 11 +- Editor/scripts/stage-build-sdk.py | 2 +- Editor/scripts/test_stage_build_sdk.py | 20 +- Package.swift | 73 +--- .../AdaEngine/AdaEngine.docc/MovieMaker.md | 4 +- .../AdaNavigation.docc/AdaNavigation.md | 6 +- Tools/Android/README.md | 12 +- Tools/Android/android.py | 22 +- Tools/Android/tests/test_android.py | 84 ++-- Tools/Tint/README.md | 2 +- script/build_and_run.sh | 4 +- script/run_navigation_demo_windows.ps1 | 44 +-- script/webgpu_validation_package.py | 23 +- 78 files changed, 158 insertions(+), 5559 deletions(-) delete mode 100644 Demos/A2UIForm/A2UIFormDemo.swift delete mode 100644 Demos/A2UIForm/Fixtures/form.jsonl delete mode 100644 Demos/A2UIForm/README.md delete mode 100755 Demos/A2UIForm/script/build_and_run.sh delete mode 100644 Demos/AndroidDemo/AndroidDemo.swift delete mode 100644 Demos/AndroidDemo/Assets/fixture.txt delete mode 100644 Demos/AndroidDemo/README.md delete mode 100644 Demos/LinuxSmoke/LinuxSmoke.swift delete mode 100644 Demos/MovieMaker/MovieMakerDemo.swift delete mode 100644 Demos/Navigation/NavigationDemoApp.swift delete mode 100644 Demos/Navigation/NavigationDemoFixture.swift delete mode 100644 Demos/Navigation/NavigationDemoMain.swift delete mode 100644 Demos/Navigation/NavigationDemoScript.swift delete mode 100644 Demos/Navigation/NavigationRobot.ada delete mode 100644 Demos/Physics2D/.codex/environments/environment.toml delete mode 100644 Demos/Physics2D/.gitignore delete mode 100644 Demos/Physics2D/Physics2DAdvancedChecks.swift delete mode 100644 Demos/Physics2D/Physics2DAdvancedDemo.swift delete mode 100644 Demos/Physics2D/Physics2DCharactersDemo.swift delete mode 100644 Demos/Physics2D/Physics2DDemoMain.swift delete mode 100644 Demos/Physics2D/Physics2DDemoScenes.swift delete mode 100644 Demos/Physics2D/Physics2DDemoView.swift delete mode 100644 Demos/Physics2D/Physics2DLargeWorldChecks.swift delete mode 100644 Demos/Physics2D/Physics2DLargeWorldDemo.swift delete mode 100644 Demos/Physics2D/Physics2DMechanismsDemo.swift delete mode 100644 Demos/Physics2D/Physics2DQueriesDemo.swift delete mode 100644 Demos/Physics2D/README.md delete mode 100755 Demos/Physics2D/script/build_and_run.sh delete mode 100644 Demos/SpineNative/Package.resolved delete mode 100644 Demos/SpineNative/Package.swift delete mode 100644 Demos/SpineNative/README.md delete mode 100644 Demos/SpineNative/SpineNativeApp.swift delete mode 100644 Demos/SpineNative/capture.py delete mode 100644 Demos/SpinePreview/Assets/SpineRuntime-LICENSE.txt delete mode 100644 Demos/SpinePreview/Assets/UPSTREAM.md delete mode 100644 Demos/SpinePreview/Assets/preview.html delete mode 100644 Demos/SpinePreview/Assets/spineboy-pro.skel delete mode 100644 Demos/SpinePreview/Assets/spineboy.atlas delete mode 100644 Demos/SpinePreview/Assets/spineboy.png delete mode 100644 Demos/SpinePreview/README.md delete mode 100644 Demos/SpinePreview/SpinePreview.swift delete mode 100644 Demos/Sponza/Camera/SponzaCameraMotion.swift delete mode 100644 Demos/Sponza/Camera/SponzaFlyCamera.swift delete mode 100644 Demos/Sponza/Camera/SponzaLightMotion.swift delete mode 100644 Demos/Sponza/Package.resolved delete mode 100644 Demos/Sponza/Package.swift delete mode 100644 Demos/Sponza/README.md delete mode 100644 Demos/Sponza/SponzaDemo.swift delete mode 100644 Demos/Sponza/SponzaDemoProfiler.swift delete mode 100644 Demos/Sponza/SponzaLighting.swift delete mode 100644 Demos/Sponza/SponzaStutterProfiler.swift delete mode 100644 Demos/Sponza/SponzaWarmup.swift delete mode 100644 Demos/Sponza/Tests/SponzaCameraTests.swift delete mode 100644 Demos/SpriteInstancingValidation/README.md delete mode 100644 Demos/SpriteInstancingValidation/SpriteValidationApp.swift delete mode 100644 Demos/SpriteInstancingValidation/SpriteValidationFixture.swift delete mode 100644 Demos/SpriteInstancingValidation/project.yml diff --git a/.agents/skills/adaengine-development/references/architecture.md b/.agents/skills/adaengine-development/references/architecture.md index 5d4f2f30a..692976de9 100644 --- a/.agents/skills/adaengine-development/references/architecture.md +++ b/.agents/skills/adaengine-development/references/architecture.md @@ -37,7 +37,7 @@ The root `AdaEngine` product is the high-level facade. Prefer implementing behav ### Tools and applications - `Editor/`: its own SwiftPM package with executable `AdaEditor`, tests, resources, package tooling, local root-AdaEngine dependency, and `project.yml` for a macOS app wrapper generated with XcodeGen. -- `Demos/`: real examples and stress/benchmark scenes. Use these to understand current API style and for manual/runtime validation, but do not treat a demo as the implementation layer. +- `AdaExamples/Examples/Technical/` (separate repository): real examples and stress/benchmark scenes. The engine checkout keeps `Demos/` ignored for local work only. Use these to understand current API style and for manual/runtime validation, but do not treat a demo as the implementation layer. - `Plugins/`: SwiftPM build/command plugins for WebGPU/Tint, web export, shader transpilation, and texture atlas generation. - `Tests/Tests`: focused tests aligned with root modules. `Editor/Tests/AdaEditorTests` belongs to the separate editor package. diff --git a/Demos/A2UIForm/A2UIFormDemo.swift b/Demos/A2UIForm/A2UIFormDemo.swift deleted file mode 100644 index e2f21d687..000000000 --- a/Demos/A2UIForm/A2UIFormDemo.swift +++ /dev/null @@ -1,199 +0,0 @@ -import AdaA2UI -import AdaEngine -@_spi(Internal) import AdaPlatform -import Foundation -import Observation -#if os(macOS) -import AppKit -#endif - -@main -struct A2UIFormDemo: App { - var body: some AppScene { - WindowGroup { FormDemoView() } - .window(with: UIWindow.Configuration(title: "AdaUI · A2UI Forms", frame: Rect(x: 160, y: 140, width: 760, height: 820))) - } -} - -@MainActor @Observable -private final class FormDemoModel { - let client = A2UIClient() - var status = "Waiting for the agent stream…" - var lastAction = "Submit the form to inspect its action payload." - var started = false - var lastEvent: A2UIClientEvent? - - init() { - client.onEvent = { [weak self] event in - guard let self else { return } - if case let .object(envelope) = event.envelope, envelope["action"] != nil { - self.lastEvent = event - self.lastAction = (try? String(decoding: JSONEncoder().encode(event.envelope), as: UTF8.self)) ?? "Action received" - demoLog("A2UI action: \(self.lastAction)") - } else { - self.status = self.client.lastError?.message ?? "Invalid message rejected" - } - } - } - - func start() async { - guard !started else { return } - started = true - do { - guard let url = Bundle.module.url(forResource: "form", withExtension: "jsonl", subdirectory: "Fixtures") else { - throw A2UIValidationError(message: "Demo fixture missing.") - } - let data = try Data(contentsOf: url) - var decoder = A2UIJSONLDecoder() - // Byte chunks exercise the same framing path a network adapter would use. - for offset in stride(from: 0, to: data.count, by: 53) { - let chunk = data.subdata(in: offset.. some View { - PlainTextFieldStyle()._body(configuration: configuration) - .padding(.horizontal, 8) - .background(Color(red: 0.16, green: 0.19, blue: 0.24)) - .border(Color(red: 0.34, green: 0.39, blue: 0.47), lineWidth: 1) - } -} - -private struct FormDemoButtonStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.label - .fontSize(14) - .padding(9) - .background(configuration.isPressed ? Color(red: 0.25, green: 0.38, blue: 0.55) : Color(red: 0.18, green: 0.26, blue: 0.37)) - } -} diff --git a/Demos/A2UIForm/Fixtures/form.jsonl b/Demos/A2UIForm/Fixtures/form.jsonl deleted file mode 100644 index dcba20a88..000000000 --- a/Demos/A2UIForm/Fixtures/form.jsonl +++ /dev/null @@ -1,5 +0,0 @@ -{"version":"v0.9.1","createSurface":{"surfaceId":"form","catalogId":"https://adaengine.org/a2ui/catalogs/forms/v1","sendDataModel":true}} -{"version":"v0.9.1","updateComponents":{"surfaceId":"form","components":[{"id":"root","component":"Column","spacing":16,"children":["heading","name","copy","enabled","role","health-label","health","submit"]},{"id":"heading","component":"Text","text":"Create an NPC profile"}]}} -{"version":"v0.9.1","updateComponents":{"surfaceId":"form","components":[{"id":"name","component":"TextField","placeholder":"NPC name","text":{"path":"/npc/name"},"checks":[{"condition":{"call":"required","args":{"value":{"path":"/npc/name"}}},"message":"NPC name is required."}]},{"id":"copy","component":"Text","text":{"path":"/npc/name"}},{"id":"enabled","component":"Toggle","label":"Enable dialog","value":{"path":"/npc/dialogEnabled"}}]}} -{"version":"v0.9.1","updateDataModel":{"surfaceId":"form","value":{"npc":{"name":"Guide","dialogEnabled":true,"role":"guide","health":50}}}} -{"version":"v0.9.1","updateComponents":{"surfaceId":"form","components":[{"id":"submit","component":"Button","text":"Create profile","action":{"event":{"name":"create_npc","context":{"name":{"path":"/npc/name"},"dialogEnabled":{"path":"/npc/dialogEnabled"},"role":{"path":"/npc/role"},"health":{"path":"/npc/health"}}}},"checks":[{"condition":{"call":"required","args":{"value":{"path":"/npc/name"}}},"message":"NPC name is required."},{"condition":{"call":"range","args":{"value":{"path":"/npc/health"},"min":1,"max":100}},"message":"Health must be between 1 and 100."}]},{"id":"role","component":"ChoicePicker","label":"Role","value":{"path":"/npc/role"},"options":[{"label":"Guide","value":"guide"},{"label":"Merchant","value":"merchant"},{"label":"Guard","value":"guard"}]},{"id":"health-label","component":"Text","text":"Health"},{"id":"health","component":"Slider","value":{"path":"/npc/health"},"min":1,"max":100,"step":1,"checks":[{"condition":{"call":"range","args":{"value":{"path":"/npc/health"},"min":1,"max":100}},"message":"Health must be between 1 and 100."}]}]}} diff --git a/Demos/A2UIForm/README.md b/Demos/A2UIForm/README.md deleted file mode 100644 index 231d8feb7..000000000 --- a/Demos/A2UIForm/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# A2UI form demo - -See [the integration guide](../../Documentation/A2UI.md). - -The NPC form includes a single-choice role picker and stepped health slider. -Required/range checks show inline messages and block invalid submission. Native -verification clears the name, checks rejection, corrects it, chooses a role, -changes health, submits, and exports controls plus validation into `.ui`. diff --git a/Demos/A2UIForm/script/build_and_run.sh b/Demos/A2UIForm/script/build_and_run.sh deleted file mode 100755 index 89248c792..000000000 --- a/Demos/A2UIForm/script/build_and_run.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -MODE="${1:-run}" -DEMO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -REPO_ROOT="$(cd "$DEMO_ROOT/../.." && pwd)" -BUILD_PATH="${ADAENGINE_A2UI_BUILD_PATH:-/private/tmp/adaengine-a2ui-build}" -mkdir -p "$BUILD_PATH" -BUILD_PATH="$(cd "$BUILD_PATH" && pwd -P)" -APP="$DEMO_ROOT/dist/A2UIFormDemo.app" -export ADAENGINE_DISABLE_SWAN=1 -export CLANG_MODULE_CACHE_PATH="$BUILD_PATH/clang-cache" -export SWIFTPM_MODULECACHE_OVERRIDE="$BUILD_PATH/swift-cache" -case "$MODE" in run|--verify|--debug|--logs|--telemetry) ;; *) echo "usage: $0 [run|--verify|--debug|--logs|--telemetry]" >&2; exit 2 ;; esac -for demo_pid in $(pgrep -f "$APP/Contents/MacOS/A2UIFormDemo" || true); do - demo_command="$(ps -p "$demo_pid" -o command= || true)" - if [[ "$demo_command" == "$APP/Contents/MacOS/A2UIFormDemo" || "$demo_command" == "$APP/Contents/MacOS/A2UIFormDemo "* ]]; then - kill "$demo_pid" - fi -done -mkdir -p "$DEMO_ROOT/dist" -if ! swift build --package-path "$REPO_ROOT" --scratch-path "$BUILD_PATH" --disable-sandbox --product A2UIFormDemo > "$DEMO_ROOT/dist/build.log" 2>&1; then - tail -60 "$DEMO_ROOT/dist/build.log" - exit 1 -fi -BIN="$(swift build --package-path "$REPO_ROOT" --scratch-path "$BUILD_PATH" --disable-sandbox --show-bin-path)" -mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" -cp "$BIN/A2UIFormDemo" "$APP/Contents/MacOS/" -for resource in "$BIN"/*.bundle; do - [[ -d "$resource" ]] && cp -R "$resource" "$APP/" -done -cat > "$APP/Contents/Info.plist" <<'PLIST' - - - -CFBundleExecutableA2UIFormDemo -CFBundleIdentifierorg.adaengine.demo.a2ui-form -CFBundleNameAdaUI A2UI Forms -CFBundlePackageTypeAPPL -LSMinimumSystemVersion15.0 -NSPrincipalClassNSApplication - -PLIST -: > "$DEMO_ROOT/dist/runtime.log" -if [[ "$MODE" == --debug ]]; then - /usr/bin/lldb -- "$APP/Contents/MacOS/A2UIFormDemo" - exit -fi -OPEN_ARGS=(-n "$APP" --stdout "$DEMO_ROOT/dist/runtime.log" --stderr "$DEMO_ROOT/dist/runtime.log" --args) -if [[ "$MODE" == --verify ]]; then OPEN_ARGS+=(--verify --proof-directory "$DEMO_ROOT/dist/proof"); fi -/usr/bin/open "${OPEN_ARGS[@]}" -case "$MODE" in - --verify) - for attempt in {1..30}; do - if rg -q 'A2UI demo verification PASS' "$DEMO_ROOT/dist/runtime.log"; then - cat "$DEMO_ROOT/dist/runtime.log" - exit 0 - fi - if rg -q 'verification FAIL|Fatal error' "$DEMO_ROOT/dist/runtime.log"; then - tail -40 "$DEMO_ROOT/dist/runtime.log" - exit 1 - fi - sleep 1 - done - tail -40 "$DEMO_ROOT/dist/runtime.log" - echo 'A2UI verification timed out' >&2 - exit 1 - ;; - --logs|--telemetry) /usr/bin/log stream --info --style compact --predicate 'process == "A2UIFormDemo"' ;; -esac diff --git a/Demos/AndroidDemo/AndroidDemo.swift b/Demos/AndroidDemo/AndroidDemo.swift deleted file mode 100644 index baaa09748..000000000 --- a/Demos/AndroidDemo/AndroidDemo.swift +++ /dev/null @@ -1,39 +0,0 @@ -#if os(Android) - import AdaEngine - import AdaPlatform - import Foundation - - @_cdecl("ada_android_start") - public func androidDemoStart() { - AndroidRuntime.start { AndroidDemo() } - } - - struct AndroidDemo: App { - var body: some AppScene { - WindowGroup( - content: { AndroidDemoView() }, - assetBundle: AndroidResourceBundle.bundle(named: "AdaEngine_AndroidDemo") - ) - } - } - - private struct AndroidDemoView: View { - @State private var taps = 0 - var body: some View { - VStack(spacing: 24) { - Text("AdaEngine on Android").foregroundColor(.white) - Text("Native Swift • Android Surface • Touch").foregroundColor(.white) - Text("Taps: \(taps)").foregroundColor(.white) - Button(action: { - taps += 1 - AndroidRuntime.log("AndroidDemo taps=\(taps)") - }) { - Text("Tap +1").foregroundColor(.white) - } - } - } - } - -#else - public enum AndroidDemoUnsupportedHost {} -#endif diff --git a/Demos/AndroidDemo/Assets/fixture.txt b/Demos/AndroidDemo/Assets/fixture.txt deleted file mode 100644 index 4331b72ff..000000000 --- a/Demos/AndroidDemo/Assets/fixture.txt +++ /dev/null @@ -1 +0,0 @@ -AdaEngine Android packaged resource diff --git a/Demos/AndroidDemo/README.md b/Demos/AndroidDemo/README.md deleted file mode 100644 index 9952c8607..000000000 --- a/Demos/AndroidDemo/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# AndroidDemo - -Native Swift AdaUI demo for the Android host. Build and run from the repository -root with `python3 Tools/Android/android.py run`. See `Tools/Android/README.md` -for matching toolchain/SDK installation and emulator/device selection. diff --git a/Demos/LinuxSmoke/LinuxSmoke.swift b/Demos/LinuxSmoke/LinuxSmoke.swift deleted file mode 100644 index d32df8ad8..000000000 --- a/Demos/LinuxSmoke/LinuxSmoke.swift +++ /dev/null @@ -1,49 +0,0 @@ -import AdaEngine -@_spi(Internal) import AdaPlatform -import Foundation - -@main -struct LinuxSmoke: App { - var body: some AppScene { - WindowGroup { - LinuxSmokeView() - } - .window( - with: UIWindow.Configuration( - title: "AdaEngine Linux", - frame: Rect(x: 80, y: 80, width: 800, height: 600), - minimumSize: Size(width: 480, height: 360) - ) - ) - } -} - -private struct LinuxSmokeView: View { - @State private var clicks = 0 - @State private var text = "Hello, Linux!" - - var body: some View { - VStack(spacing: 24) { - Text("AdaEngine on Linux") - .font(.system(size: 36)) - Text("Resize the window, type a message and press the button.") - TextField("Message", text: $text) - .frame(width: 360, height: 44) - Button("Clicks: \(clicks)") { - clicks += 1 - smokeLog("LINUX_SMOKE_CLICK \(clicks) text=\(text)") - } - Text(text) - } - .padding(32) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.white) - .task { - smokeLog("LINUX_SMOKE_READY backend=\(RenderEngine.shared.type)") - } - } -} - -private func smokeLog(_ message: String) { - FileHandle.standardOutput.write(Data((message + "\n").utf8)) -} diff --git a/Demos/MovieMaker/MovieMakerDemo.swift b/Demos/MovieMaker/MovieMakerDemo.swift deleted file mode 100644 index 789b1df72..000000000 --- a/Demos/MovieMaker/MovieMakerDemo.swift +++ /dev/null @@ -1,75 +0,0 @@ -import AdaEngine -import Math - -/// An asset-free take that exercises the same renderer and mixer used by games. -@main -struct MovieMakerDemo { - @MainActor - static func main() async throws { - let configuration = try MovieRecordingConfiguration.commandLine() ?? MovieRecordingConfiguration( - outputURL: URL(fileURLWithPath: "Movies/demo.png"), - width: 640, - height: 360, - frameCount: 180 - ) - let maker = MovieMaker() - let scene = MovieDemoScene(configuration: configuration) - defer { scene.music?.stop() } - let recording = try await maker.record(configuration: configuration, make: { app in - scene.configure(app) - }, update: { world, frame in - try scene.update(world: world, frame: frame) - }, progress: { count in - if count % configuration.framesPerSecond == 0 { print("Recorded \(count) frames") } - }) - print("Saved \(recording.frameCount) frames (\(recording.duration) seconds) to \(configuration.outputURL.deletingLastPathComponent().path)") - } -} - -@MainActor -private final class MovieDemoScene { - let configuration: MovieRecordingConfiguration - var sprites: [Entity] = [] - var music: AudioPlaybackController? - - init(configuration: MovieRecordingConfiguration) { self.configuration = configuration } - - func configure(_ app: AppWorlds) { - app.addPlugin(TransformPlugin()) - .addPlugin(RenderWorldPlugin()) - .addPlugin(CameraPlugin()) - .addPlugin(VisibilityPlugin()) - .addPlugin(Core2DPlugin()) - .addPlugin(SpritePlugin()) - .addPlugin(AudioPlugin()) - var camera = Camera() - camera.backgroundColor = Color(red: 0.025, green: 0.035, blue: 0.08) - app.spawn("TrailerCamera", bundle: Camera2D(camera: camera)) - } - - func update(world: World, frame: MovieFrame) throws { - if sprites.isEmpty { - for index in 0..<9 { - let hue = Float(index) / 8 - let color = Color(red: 0.25 + hue * 0.7, green: 0.85 - hue * 0.4, blue: 1 - hue * 0.35) - sprites.append(world.spawn("Light \(index)") { - Sprite(tintColor: color, size: [32, 32]) - Transform() - }) - } - var spec = SoundSpec() - spec.peak = 0.15 - spec.voices = [SoundVoice(wave: .sine, frequency: 220, duration: 1)] - let resource = try AudioResource.create(from: WAVWriter().encode(SoundSynth.render(spec))) - music = AudioServer.shared.prepareAudio(resource).setLoop(true) - music?.play() - } - let time = Float(frame.time) - for (index, entity) in sprites.enumerated() { - let phase = time * 1.4 + Float(index) * .pi * 2 / 9 - let x = Math.cos(phase) * Float(configuration.width) * 0.32 - let y = Math.sin(phase * 2) * Float(configuration.height) * 0.28 - entity.components += Transform(rotation: Quat(axis: [0, 0, 1], angle: phase), position: [x, y, 0]) - } - } -} diff --git a/Demos/Navigation/NavigationDemoApp.swift b/Demos/Navigation/NavigationDemoApp.swift deleted file mode 100644 index 716abf0e7..000000000 --- a/Demos/Navigation/NavigationDemoApp.swift +++ /dev/null @@ -1,201 +0,0 @@ -import AdaEngine -import Foundation -import Observation - -@MainActor -@Observable -final class NavigationDemoSession: Resource { - var status = "Готовим площадку…" - var showNavigation = true - var rebakeRequested = false - var nextGoalRequested = false - var resetRequested = false - var patrolRequested = false - var script: AdaScriptPlugin? - var triangles: [Vector3] = [] - var robot: Entity.ID? - var goalIndex = 0 - var wasMouseDown = false - - func setStatus(_ value: String) { - if status != value { status = value } - } -} - -struct NavigationDemoApp: App { - var body: some AppScene { - WindowGroup { NavigationDemoView() } - .window(with: UIWindow.Configuration(title: "AdaEngine · Navigation playground", frame: Rect(x: 80, y: 60, width: 1100, height: 800))) - } -} - -private struct NavigationDemoView: View { - @State private var session = NavigationDemoSession() - - var body: some View { - VStack(spacing: 12) { - Text("Проложи путь для робота").font(.system(size: 24)) - Text("Нажми на пол — робот найдёт дорогу вокруг препятствий.") - HStack(spacing: 18) { - Button("Другая цель") { session.nextGoalRequested = true } - Button(session.showNavigation ? "Скрыть сетку" : "Показать сетку") { session.showNavigation.toggle() } - Button("В начало") { session.resetRequested = true } - Button("Запечь заново") { session.rebakeRequested = true } - Button("Патруль AdaScript") { session.patrolRequested = true } - } - Text(session.status) - SceneView(useSceneCameras: true, make: { app in - configureNavigationDemo(&app, session: session) - }, updateContent: { _, _ in }) - .frameCaptureLabel("NavigationDemo") - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - .padding(16) - .background(Color(0.06, 0.08, 0.13, 1)) - .foregroundColor(.white) - } -} - -@MainActor -func configureNavigationDemo(_ app: inout AppWorlds, session: NavigationDemoSession) { - app.addPlugin(TransformPlugin()).addPlugin(InputPlugin()).addPlugin(RenderWorldPlugin()) - .addPlugin(CameraPlugin()).addPlugin(AssetsPlugin()).addPlugin(VisibilityPlugin()) - .addPlugin(ScenePlugin()).addPlugin(Core3DPlugin(includes2D: false)).addPlugin(UpscalePlugin()) - .addPlugin(Physics3DPlugin()).addPlugin(Navigation3DPlugin()).addPlugin(NavigationDemoPlugin()) - app.insertResource(session) -} - -/// SceneView installs its schedulers after make(); register systems when plugins are built. -private struct NavigationDemoPlugin: Plugin { - func setup(in app: AppWorlds) { - app.addSystem(NavigationDemoStartupSystem.self, on: .startup) - app.addSystem(NavigationDemoControlsSystem.self, on: .update) - app.addSystem(NavigationDemoOverlaySystem.self, on: .postUpdate) - do { - app.main.getResource(NavigationDemoSession.self)?.script = try NavigationDemoScript.install(in: app) - } catch { - app.main.getResource(NavigationDemoSession.self)?.setStatus("Ошибка AdaScript: \(error)") - } - if CommandLine.arguments.contains("--capture") { - app.getSubworldBuilder(by: .renderWorld)?.getResource(RenderGraphDiagnostics.self)?.configure(isEnabled: true) - } - } -} - -@System -@MainActor -func NavigationDemoStartup(_ context: WorldUpdateContext, _ session: Res) async { - let robot = NavigationDemoFixture.installPhysics(in: context.world) - session.wrappedValue.robot = robot.id - robot.components[NavigationDemoPatrolComponent.self] = NavigationDemoPatrolComponent() - robot.components[NavigationTarget3DComponent.self] = NavigationTarget3DComponent() - NavigationDemoFixture.installGraphics(in: context.world, robot: robot) - do { - let mesh = try await NavigationMesh(data: NavigationDemoFixture.bakedAsset()) - context.world.insertResource(NavigationMap3D(mesh: mesh)) - session.wrappedValue.triangles = try await mesh.debugTriangles() - } catch { - session.wrappedValue.status = "Не удалось построить сетку: \(error)" - } -} - -@PlainSystem(dependencies: [.before(NavigationPathRequest3DSystem.self)]) -struct NavigationDemoControlsSystem { - @Res private var session - @Res private var input - @Query private var cameras - // Reset/picking use the robot's stable ID; declare those component writes to the scheduler. - @Query, Ref, Ref, Ref, Ref> - private var robotAccess - - init(world _: World) {} - - @MainActor - func update(context: UpdateContext) async { - let state = session - _ = robotAccess - guard let id = state.robot, let robot = context.world.getEntityByID(id) else { return } - if state.resetRequested { - robot.components[NavigationDemoPatrolComponent.self] = NavigationDemoPatrolComponent(enabled: false) - state.resetRequested = false - robot.components[Transform.self] = Transform(position: NavigationDemoFixture.start) - var controller = robot.components[CharacterController3DComponent.self] ?? CharacterController3DComponent() - controller.resetMotion = true - robot.components[CharacterController3DComponent.self] = controller - // Invalidate the pending request even when returning to the same goal. - robot.components[NavigationAgent3DComponent.self] = NavigationAgent3DComponent() - } - if state.nextGoalRequested { - robot.components[NavigationDemoPatrolComponent.self] = NavigationDemoPatrolComponent(enabled: false) - state.nextGoalRequested = false - state.goalIndex = (state.goalIndex + 1) % NavigationDemoFixture.goals.count - robot.components[NavigationTarget3DComponent.self] = NavigationTarget3DComponent(destination: NavigationDemoFixture.goals[state.goalIndex]) - } - let down = input.isMouseButtonPressed(.left) - if down && !state.wasMouseDown, let (camera, transform) = cameras.first, - let ray = camera.viewportToWorld( - cameraGlobalTransform: transform.matrix, - point: Vector2(input.getMousePosition().x, camera.logicalViewport.rect.size.height - input.getMousePosition().y) - ), - abs(ray.direction.y) > 0.0001 { - let distance = -ray.origin.y / ray.direction.y - let point = ray.origin + ray.direction * distance - if distance > 0, abs(point.x) < 10, abs(point.z) < 7 { - robot.components[NavigationDemoPatrolComponent.self] = NavigationDemoPatrolComponent(enabled: false) - robot.components[NavigationTarget3DComponent.self] = NavigationTarget3DComponent(destination: [point.x, 0, point.z]) - } - } - state.wasMouseDown = down - if state.patrolRequested { - state.patrolRequested = false - robot.components[NavigationDemoPatrolComponent.self] = NavigationDemoPatrolComponent() - robot.components[NavigationTarget3DComponent.self] = NavigationTarget3DComponent() - } - if state.rebakeRequested { - state.rebakeRequested = false - state.status = "Запекаем площадку…" - do { - let mesh = try await NavigationMesh(data: NavigationDemoFixture.bakedAsset()) - context.world.insertResource(NavigationMap3D(mesh: mesh)) - state.triangles = try await mesh.debugTriangles() - } catch { - state.status = "Ошибка запекания: \(error)" - } - } - } -} - -@System -@MainActor -func NavigationDemoOverlay( - _ session: Res, - _ agents: Query, - _ draw: Res -) { - let state = session.wrappedValue - if state.showNavigation { - let triangles = state.triangles - for offset in stride(from: 0, to: triangles.count, by: 3) { - draw.wrappedValue.polyline(Array(triangles[offset..<(offset + 3)]).map { $0 + Vector3(0, 0.06, 0) }, closed: true, color: Color(0.15, 0.85, 0.7, 1)) - } - } - agents.forEach { agent, target, transform in - if let path = agent.follower.path { - draw.wrappedValue.polyline(path.points.map { $0 + Vector3(0, 0.16, 0) }, color: .yellow, depthTested: false) - } - if let destination = target.destination { - draw.wrappedValue.sphere(center: destination + Vector3(0, 0.2, 0), radius: 0.3, color: agent.lastError == nil ? .yellow : .red) - } - draw.wrappedValue.sphere(center: transform.position + Vector3(0, 0.06, 0), radius: 0.4, color: .mint) - if let error = agent.lastError { - state.setStatus(error == .targetOutsideMesh || error == .unreachable ? "Здесь нет прохода. Выбери другую точку." : "Ошибка маршрута: \(error)") - } else { - switch agent.follower.state { - case .following: state.setStatus("Идём к цели · поворотов: \(max(0, (agent.follower.path?.points.count ?? 0) - 2))") - case .arrived: state.setStatus("Пришли! Выбери следующую цель.") - case .blocked: state.setStatus("Проход занят — ищем путь снова…") - default: break - } - } - } -} diff --git a/Demos/Navigation/NavigationDemoFixture.swift b/Demos/Navigation/NavigationDemoFixture.swift deleted file mode 100644 index 9af5141fe..000000000 --- a/Demos/Navigation/NavigationDemoFixture.swift +++ /dev/null @@ -1,115 +0,0 @@ -import AdaEngine -import AdaNavigationBaking -import Foundation - -/// The same geometry drives the bake, physical colliders, and visible meshes. -@MainActor -enum NavigationDemoFixture { - struct Obstacle { - let center: Vector3 - let size: Vector3 - } - - static let start = Vector3(-8, 0, -4) - static let goals: [Vector3] = [[8, 0, 4], [-8, 0, 5], [8, 0, -5], [0, 0, 5]] - static let obstacles = [ - Obstacle(center: [0, 1.5, 0], size: [4, 3, 4]), - Obstacle(center: [-5.5, 1.1, 0], size: [1.4, 2.2, 6]), - Obstacle(center: [5, 1.1, 2.5], size: [2, 2.2, 3]) - ] - - static func bakedAsset() async throws -> NavigationMeshData { - var vertices: [Vector3] = [[-10, 0, -7], [-10, 0, 7], [10, 0, 7], [10, 0, -7]] - var indices: [UInt32] = [0, 1, 2, 0, 2, 3] - for box in obstacles { - let min = box.center - box.size / 2 - let max = box.center + box.size / 2 - let corners: [Vector3] = [ - [min.x, min.y, min.z], [min.x, min.y, max.z], [max.x, min.y, max.z], [max.x, min.y, min.z], - [min.x, max.y, min.z], [min.x, max.y, max.z], [max.x, max.y, max.z], [max.x, max.y, min.z] - ] - let offset = UInt32(vertices.count) - vertices += corners - // Outward winding: ceiling on the bottom, walkable top, and blocking walls. - indices += [0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7, - 0, 1, 5, 0, 5, 4, 3, 7, 6, 3, 6, 2, - 0, 4, 7, 0, 7, 3, 1, 2, 6, 1, 6, 5].map { offset + $0 } - } - let baked = try await NavigationMeshBaker.bake(vertices: vertices, indices: indices) - let directory = FileManager.default.temporaryDirectory.appendingPathComponent("AdaEngine-NavigationDemo", isDirectory: true) - try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - let url = directory.appendingPathComponent("playground.navmesh") - try baked.encoded().write(to: url, options: .atomic) - // Exercise the production file decoder before installing the map. - return try NavigationMeshData(encoded: Data(contentsOf: url)) - } - - @discardableResult - static func installPhysics(in world: World) -> Entity { - func box(_ name: String, center: Vector3, size: Vector3) { - world.spawn(name) { - Transform(position: center) - PhysicsBody3DComponent(shapes: [.generateBox(width: size.x, height: size.y, depth: size.z)], mode: .static) - } - } - box("Floor", center: [0, -0.3, 0], size: [20, 0.6, 14]) - for (index, obstacle) in obstacles.enumerated() { box("Obstacle \(index)", center: obstacle.center, size: obstacle.size) } - return world.spawn("Navigation robot") { - Transform(position: start) - CharacterController3DComponent() - NavigationAgent3DComponent() - NavigationTarget3DComponent(destination: goals[0]) - } - } - - static func installGraphics(in world: World, robot: Entity) { - let device = unsafe RenderEngine.shared.renderDevice - func material(_ color: Vector4) -> PBRMaterial { - let result = PBRMaterial() - result.baseColorFactor = color - result.metallicFactor = 0 - result.roughnessFactor = 0.75 - return result - } - let cube = Mesh.generateCube(renderDevice: device) - for entity in world.getEntities() where entity.components[PhysicsBody3DComponent.self] != nil { - let size = entity.name == "Floor" ? Vector3(20, 0.6, 14) - : obstacles[Int(entity.name.split(separator: " ").last ?? "0") ?? 0].size - var transform = entity.components[Transform.self] ?? Transform() - transform.scale = size - entity.components[Transform.self] = transform - entity.components[Mesh3DComponent.self] = Mesh3DComponent( - mesh: cube, materials: [material(entity.name == "Floor" ? [0.18, 0.24, 0.31, 1] : [0.55, 0.48, 0.8, 1])] - ) - } - let sphere = Mesh.generateSphere(radius: 1, segments: 24, rings: 16, renderDevice: device) - func part(_ name: String, at position: Vector3, scale: Vector3, color: Vector4) { - let entity = world.spawn(name) { - Transform(scale: scale, position: position) - Mesh3DComponent(mesh: sphere, materials: [material(color)]) - } - robot.addChild(entity) - } - part("Body", at: [0, 0.7, 0], scale: [0.36, 0.55, 0.32], color: [0.05, 0.8, 0.85, 1]) - part("Head", at: [0, 1.5, 0], scale: [0.32, 0.32, 0.32], color: [1, 0.8, 0.2, 1]) - for x: Float in [-0.12, 0.12] { - part("Eye", at: [x, 1.55, -0.28], scale: [0.055, 0.075, 0.045], color: [0.03, 0.05, 0.1, 1]) - } - var camera = Camera() - camera.backgroundColor = Color(0.035, 0.055, 0.09, 1) - let eye = Vector3(12, 16, -18) - let forward = (-eye).normalized - let right = Vector3.up.cross(forward).normalized - let up = forward.cross(right) - let cameraMatrix = Transform3D(columns: [Vector4(right, 0), Vector4(up, 0), Vector4(forward, 0), Vector4(eye, 1)]) - world.spawn("Playground camera", bundle: Camera3D( - camera: camera, - perspectiveProjection: PerspectiveProjection(fieldOfView: .degrees(45)), - transform: Transform(matrix: cameraMatrix) - )) - world.spawn("Sun") { - Transform(rotation: Quat(axis: .right, angle: .pi / 3)) - DirectionalLightComponent(intensity: 3, castShadows: true) - } - } -} diff --git a/Demos/Navigation/NavigationDemoMain.swift b/Demos/Navigation/NavigationDemoMain.swift deleted file mode 100644 index e8d1fccf3..000000000 --- a/Demos/Navigation/NavigationDemoMain.swift +++ /dev/null @@ -1,148 +0,0 @@ -@_spi(Internal) import AdaApp -import AdaEngine -import Foundation - -@main -@MainActor -struct NavigationDemoMain { - static func main() async throws { - if CommandLine.arguments.contains("--validate-script") { - try await validateScript() - } else if CommandLine.arguments.contains("--validate") { - try await validate() - } else if let index = CommandLine.arguments.firstIndex(of: "--capture"), CommandLine.arguments.indices.contains(index + 1) { - try await capture(to: CommandLine.arguments[index + 1]) - } else { - try await AppRuntime.run(NavigationDemoApp()) - } - } - - private static func validateScript() async throws { - let app = AppWorlds(main: World()) - app.addPlugin(MainSchedulerPlugin()).addPlugin(Physics3DPlugin()).addPlugin(Navigation3DPlugin()) - try await app.build() - let script = try NavigationDemoScript.install(in: app) - let robot = NavigationDemoFixture.installPhysics(in: app.main) - robot.components[NavigationDemoPatrolComponent.self] = NavigationDemoPatrolComponent() - robot.components[NavigationTarget3DComponent.self] = NavigationTarget3DComponent() - app.insertResource(NavigationMap3D(mesh: try await NavigationMesh(data: NavigationDemoFixture.bakedAsset()))) - app.main.insertResource(FixedTime(deltaTime: 1.0 / 60)) - await app.main.runScheduler(.physicsSync) - for _ in 0..<4800 { - await app.main.runScheduler(.update) - await app.main.runScheduler(.physicsStep) - await app.main.runScheduler(.physicsWriteback) - if robot.components[NavigationDemoPatrolComponent.self]?.completedGoals == 4 { break } - } - guard robot.components[NavigationDemoPatrolComponent.self]?.completedGoals == 4, script.diagnostics.isEmpty else { - throw DemoFailure.didNotArrive - } - print("PASS: NavigationRobot.ada completed four goals through the navigation and physics systems") - } - - /// Runs the demo's actual bake/file load/target requests/physical movement without a GPU. - private static func validate() async throws { - let app = AppWorlds(main: World()) - app.addPlugin(MainSchedulerPlugin()).addPlugin(Physics3DPlugin()).addPlugin(Navigation3DPlugin()) - try await app.build() - let robot = NavigationDemoFixture.installPhysics(in: app.main) - let mesh = try await NavigationMesh(data: NavigationDemoFixture.bakedAsset()) - app.insertResource(NavigationMap3D(mesh: mesh)) - app.main.insertResource(FixedTime(deltaTime: 1.0 / 60)) - await app.main.runScheduler(.physicsSync) - for goal in NavigationDemoFixture.goals { - robot.components[NavigationTarget3DComponent.self] = NavigationTarget3DComponent(destination: goal) - for _ in 0..<1200 { - await app.main.runScheduler(.update) - await app.main.runScheduler(.physicsStep) - await app.main.runScheduler(.physicsWriteback) - if robot.components[NavigationAgent3DComponent.self]?.follower.state == .arrived { break } - } - guard robot.components[NavigationAgent3DComponent.self]?.follower.state == .arrived else { - throw DemoFailure.didNotArrive - } - print("PASS: reached \(goal)") - } - print("PASS: saved/loaded \(mesh.data.polygonCount) polygons; four goals reached through the physics controller") - } - - private static func capture(to path: String) async throws { - weak var capturedApp: AppWorlds? - let runtime = OffscreenSceneRuntime(label: "NavigationDemo", size: [1100, 760], useSceneCameras: true) { app in - configureNavigationDemo(&app, session: NavigationDemoSession()) - capturedApp = app - } - try await runtime.start(timeout: .seconds(30)) - var completed = false - defer { - if !completed, let app = capturedApp { - let status = app.main.getResource(NavigationDemoSession.self)?.status ?? "missing" - report("Capture diagnostics: frame=\(runtime.frameSource.frameSequence), cameras=\(runtime.frameSource.activeCameraNames)") - report("Entities=\(app.main.getEntities().count), status=\(status)") - if let render = app.getSubworldBuilder(by: .renderWorld) { - report("Render entities: \(render.main.getEntities().map(\.name))") - if let records = render.getResource(RenderGraphDiagnostics.self)?.recentFrames(limit: 1), - let data = try? JSONEncoder().encode(records) { report(String(decoding: data, as: UTF8.self)) } - } - } - runtime.stop() - } - var frame = try await runtime.frameSource.captureFrame(after: 0, timeout: .seconds(30)) - try write(frame.image, to: path) - for _ in 0..<20 { - frame = try await runtime.frameSource.captureFrame(after: frame.sequence, timeout: .seconds(10)) - } - try write(frame.image, to: path) - guard let world = runtime.frameSource.world, - let cameraEntity = world.getEntities().first(where: { $0.components[Camera.self] != nil }), - let camera = cameraEntity.components[Camera.self], - let transform = cameraEntity.components[GlobalTransform.self], - let session = world.getResource(NavigationDemoSession.self), - let robotID = session.robot, let robot = world.getEntityByID(robotID) else { throw DemoFailure.sceneUnavailable } - let probe = Vector3(7.5, 0, 4.5) - let clip = camera.projection.makeClipView() * (transform.matrix.inverse * Vector4(probe, 1)) - guard clip.w.isFinite, clip.w > 0 else { throw DemoFailure.sceneUnavailable } - let projected = clip.xyz / clip.w - guard projected.x.isFinite, projected.y.isFinite, projected.z >= 0, projected.z <= 1 else { - throw DemoFailure.sceneUnavailable - } - let size = camera.logicalViewport.rect.size - let pointer = Vector2((projected.x + 1) * size.width / 2, (1 - projected.y) * size.height / 2) - let window = world.getResource(PrimaryWindowId.self)?.windowId ?? RID() - runtime.frameSource.movePointer(to: pointer) - runtime.frameSource.send(MouseEvent(window: window, button: .left, mousePosition: pointer, phase: .began, modifierKeys: [], time: 1)) - for _ in 0..<10 { - frame = try await runtime.frameSource.captureFrame(after: frame.sequence, timeout: .seconds(10)) - if let selected = robot.components[NavigationTarget3DComponent.self]?.destination, (selected - probe).length < 0.02 { break } - } - runtime.frameSource.send(MouseEvent(window: window, button: .left, mousePosition: pointer, phase: .ended, modifierKeys: [], time: 2)) - for _ in 0..<8 { - frame = try await runtime.frameSource.captureFrame(after: frame.sequence, timeout: .seconds(10)) - } - guard let selected = robot.components[NavigationTarget3DComponent.self]?.destination, - (selected - probe).length < 0.02 else { throw DemoFailure.pickingFailed } - print("PASS: viewport click selected \(selected)") - try write(frame.image, to: path) - completed = true - print("PASS: captured real GPU frame \(frame.sequence) to \(path)") - } - - private static func write(_ image: Image, to path: String) throws { - var bytes = Array(image.data) - if image.format == .bgra8 || image.format == .bgra8_sRGB { - for index in stride(from: 0, to: bytes.count, by: 4) { bytes.swapAt(index, index + 2) } - } - let png = try PNGWriter().encode(width: image.width, height: image.height, rgba: bytes) - try png.write(to: URL(fileURLWithPath: path), options: .atomic) - } - - private static func report(_ message: String) { - FileHandle.standardError.write(Data((message + "\n").utf8)) - } -} - -private enum DemoFailure: Error { - case didNotArrive - case sceneUnavailable - case pickingFailed -} diff --git a/Demos/Navigation/NavigationDemoScript.swift b/Demos/Navigation/NavigationDemoScript.swift deleted file mode 100644 index 45d947990..000000000 --- a/Demos/Navigation/NavigationDemoScript.swift +++ /dev/null @@ -1,24 +0,0 @@ -import AdaEngine -import Foundation - -@Component -struct NavigationDemoPatrolComponent { - var enabled: Bool = true - var completedGoals: Int = 0 -} - -@MainActor -enum NavigationDemoScript { - static func install(in app: AppWorlds) throws -> AdaScriptPlugin { - RuntimeTypeRegistry.registerComponent( - NavigationDemoPatrolComponent.self, names: ["NavigationDemoPatrolComponent"], - makeDefault: { NavigationDemoPatrolComponent() } - ) - guard let file = Bundle.module.url(forResource: "NavigationRobot", withExtension: "ada") else { - throw CocoaError(.fileNoSuchFile) - } - let script = try AdaScriptPlugin(source: String(contentsOf: file, encoding: .utf8), name: "NavigationRobot") - script.setup(in: app) - return script - } -} diff --git a/Demos/Navigation/NavigationRobot.ada b/Demos/Navigation/NavigationRobot.ada deleted file mode 100644 index 5e48891c4..000000000 --- a/Demos/Navigation/NavigationRobot.ada +++ /dev/null @@ -1,20 +0,0 @@ -// Navigation runs in the engine. This script chooses goals and reads the agent's state. -@system(id: "navigation.demo.patrol") -class RobotPatrol { - @query(NavigationAgent3DComponent, NavigationTarget3DComponent, NavigationDemoPatrolComponent) var robots; - var goals = [[8.0, 0.0, 4.0], [-8.0, 0.0, 5.0], [8.0, 0.0, -5.0], [0.0, 0.0, 5.0]]; - - func update(context) { - for (var row in robots) { - if (!row.navigationDemoPatrolComponent.enabled) { continue; } - if (row.navigationAgent3DComponent.hasArrived) { - row.navigationDemoPatrolComponent.completedGoals = row.navigationDemoPatrolComponent.completedGoals + 1; - row.navigationTarget3DComponent.destination = null; - } - if (row.navigationTarget3DComponent.destination == null) { - var next = row.navigationDemoPatrolComponent.completedGoals % 4; - row.navigationTarget3DComponent.destination = goals[next]; - } - } - } -} diff --git a/Demos/Physics2D/.codex/environments/environment.toml b/Demos/Physics2D/.codex/environments/environment.toml deleted file mode 100644 index 339cbc4fb..000000000 --- a/Demos/Physics2D/.codex/environments/environment.toml +++ /dev/null @@ -1,8 +0,0 @@ -version = 1 -name = "Box2D playground" -[setup] -script = "" -[[actions]] -name = "Run" -icon = "run" -command = "./script/build_and_run.sh" diff --git a/Demos/Physics2D/.gitignore b/Demos/Physics2D/.gitignore deleted file mode 100644 index 178135c2b..000000000 --- a/Demos/Physics2D/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/dist/ diff --git a/Demos/Physics2D/Physics2DAdvancedChecks.swift b/Demos/Physics2D/Physics2DAdvancedChecks.swift deleted file mode 100644 index 1a045f0f2..000000000 --- a/Demos/Physics2D/Physics2DAdvancedChecks.swift +++ /dev/null @@ -1,81 +0,0 @@ -import AdaEngine - -extension Physics2DDemoMain { - static func validateAdvanced(_ app: AppWorlds, _ session: PhysicsDemoSession) async throws { - let state = session.advanced - guard state.pointCount > 0, let air = state.air, let projectile = state.projectile else { throw Failure.advanced } - state.windMode = 1; state.dirty = true - await advance(app, steps: 30) - let speed = air.components[PhysicsBody2DComponent.self]?.linearVelocity.x ?? 0 - guard speed > 0.2 else { throw Failure.advanced } - state.windMode = 2; state.dirty = true - await advance(app, steps: 20) - guard (air.components[PhysicsBody2DComponent.self]?.linearVelocity.x ?? 0) < speed else { throw Failure.advanced } - state.locked = true; state.dirty = true - await advance(app, steps: 2) - guard abs(air.components[PhysicsBody2DComponent.self]?.linearVelocity.x ?? 1) < 0.01 else { throw Failure.advanced } - state.locked = false; state.damping = true; state.softSolver = true - state.reverseBelt = true; state.rolling = false; state.lowGrip = true; state.dirty = true - await advance(app, steps: 2) - guard app.main.physicsWorld2D?.solverSettings.restitutionIterations == 8, - state.belt?.components[Collision2DComponent.self]?.material.surfaceSpeed == -1.8, - state.circle?.components[PhysicsBody2DComponent.self]?.material.mixing.friction == .minimum else { throw Failure.advanced } - air.components[PhysicsBody2DComponent.self]?.setAwake(true) - state.sleepRequested = true - await advance(app, steps: 1) - guard air.components[PhysicsBody2DComponent.self]?.isAwake == false else { throw Failure.advanced } - state.sleepRequested = true; state.bullet = false; state.dirty = true; state.refireRequested = true - await advance(app, steps: 30) - guard (projectile.components[PhysicsBody2DComponent.self]?.worldCenter.x ?? -1) > 0 else { throw Failure.advanced } - state.bullet = true; state.dirty = true; state.refireRequested = true - await advance(app, steps: 30) - guard (projectile.components[PhysicsBody2DComponent.self]?.worldCenter.x ?? 1) < 0, state.hitPoints > 0 else { throw Failure.advanced } - report("PASS: advanced · wind / drag / locks / sleep / materials / CCD / contacts / solver") - } - - static func verifyAdvanced(to directory: String, tap: (String) throws -> Void) async throws { - try tap("physics-demo.advanced") - let source = try await waitForSource("Physics2D Advanced") - guard let session = source.world?.getResource(PhysicsDemoSession.self), let air = session.advanced.air else { throw Failure.advanced } - let state = session.advanced - try tap("physics-demo.advanced-wind") - _ = try await nextFrames(source, count: 30) - let speed = air.components[PhysicsBody2DComponent.self]?.linearVelocity.x ?? 0 - guard speed > 0.2 else { throw Failure.advanced } - try tap("physics-demo.advanced-wind") - _ = try await nextFrames(source, count: 20) - guard (air.components[PhysicsBody2DComponent.self]?.linearVelocity.x ?? 0) < speed else { throw Failure.advanced } - try tap("physics-demo.advanced-lock") - _ = try await nextFrames(source) - guard air.components[PhysicsBody2DComponent.self]?.motion.lockTranslationX == true, - abs(air.components[PhysicsBody2DComponent.self]?.linearVelocity.x ?? 1) < 0.01 else { throw Failure.advanced } - try tap("physics-demo.advanced-lock") - _ = try await nextFrames(source) - if air.components[PhysicsBody2DComponent.self]?.isAwake == false { try tap("physics-demo.advanced-sleep"); _ = try await nextFrames(source) } - try tap("physics-demo.advanced-sleep") - _ = try await nextFrames(source) - guard air.components[PhysicsBody2DComponent.self]?.isAwake == false else { throw Failure.advanced } - try tap("physics-demo.advanced-sleep") - try tap("physics-demo.advanced-damping") - try tap("physics-demo.advanced-belt") - try tap("physics-demo.advanced-rolling") - try tap("physics-demo.advanced-grip") - try tap("physics-demo.advanced-solver") - _ = try await nextFrames(source) - guard source.world?.physicsWorld2D?.solverSettings.contactHertz == 8, - air.components[PhysicsBody2DComponent.self]?.dynamics.linearDamping == 2, - state.circle?.components[PhysicsBody2DComponent.self]?.material.rollingResistance == 0, - state.belt?.components[Collision2DComponent.self]?.material.surfaceSpeed == -1.8 else { throw Failure.advanced } - try tap("physics-demo.advanced-bullet") - _ = try await nextFrames(source, count: 30) - guard (state.projectile?.components[PhysicsBody2DComponent.self]?.worldCenter.x ?? -1) > 0 else { throw Failure.advanced } - try tap("physics-demo.advanced-bullet") - let frame = try await nextFrames(source, count: 30) - guard (state.projectile?.components[PhysicsBody2DComponent.self]?.worldCenter.x ?? 1) < 0, - state.pointCount > 0, state.hitPoints > 0 else { throw Failure.advanced } - try write(frame.image, to: directory + "/advanced-physics.png") - try tap("physics-demo.advanced-reset") - _ = try await nextFrames(source) - report("PASS: native advanced · wind / drag / locks / sleep / materials / CCD / contact points / solver controls") - } -} diff --git a/Demos/Physics2D/Physics2DAdvancedDemo.swift b/Demos/Physics2D/Physics2DAdvancedDemo.swift deleted file mode 100644 index ba90de14e..000000000 --- a/Demos/Physics2D/Physics2DAdvancedDemo.swift +++ /dev/null @@ -1,202 +0,0 @@ -import AdaEngine -import Foundation -import Math -import Observation - -@MainActor -@Observable -final class PhysicsAdvancedState { - var windMode = 0 - var reverseBelt = false - var rolling = true - var lowGrip = false - var damping = false - var bullet = true - var locked = false - var softSolver = false - var dirty = true - var sleepRequested = false - var resetRequested = false - var refireRequested = false - var belt: Entity? - var box: Entity? - var circle: Entity? - var air: Entity? - var projectile: Entity? - var markers: [Entity] = [] - var defaults: PhysicsSolver2DSettings? - var pointCount = 0 - var hitPoints = 0 - var status = "Waiting for contacts" -} - -@MainActor -func setupAdvanced(in world: World, session: PhysicsDemoSession) throws { - let state = session.advanced - state.defaults = world.physicsWorld2D?.solverSettings - world.insertResource(PhysicsDebugOptions([.showPhysicsShapes])) - let beltMaterial = try PhysicsMaterial.generate2D(surfaceSpeed: 1.8) - state.belt = world.spawn("Conveyor") { - Collision2DComponent(shapes: [.generateBox(width: 17, height: 0.4)], material: beltMaterial) - Sprite(tintColor: Color(0.24, 0.35, 0.5, 1), size: [17, 0.4]) - Transform(position: [0, -3.3, 0]) - } - for x: Float in [-8.5, 8.5] { - world.spawn("Boundary") { Collision2DComponent(shapes: [.generateBox(width: 0.25, height: 8)]); Transform(position: [x, 0, 0]) } - } - state.box = world.spawn("Conveyor box") { - PhysicsBody2DComponent(shapes: [.generateBox(width: 0.8, height: 0.8)], mass: 1) - Sprite(tintColor: Color(1, 0.65, 0.25, 1), size: [0.8, 0.8]) - Transform(position: [-3, -2.7, 0]) - } - var circle = PhysicsBody2DComponent(shapes: [.generateCircle(radius: 1.2)], mass: 1) - circle.debugColor = Color(0.3, 1, 0.5, 1) - circle.linearVelocity = [2, 0]; circle.angularVelocity = -2 / 0.6 - state.circle = world.spawn("Rolling circle") { circle; Transform(position: [-6, -2.5, 0]) } - let capsule = try Shape2DResource.generateCapsule(from: [0, -0.35], to: [0, 0.35], radius: 0.25) - var air = PhysicsBody2DComponent(shapes: [capsule], mass: 2) - air.gravityScale = 0; air.debugColor = Color(0.2, 0.85, 1, 1) - state.air = world.spawn("Wind capsule") { air; PhysicsWind2DComponent(.init(drag: 0)); Transform(position: [0, 0.7, 0]) } - world.spawn("Thin kinematic CCD target") { - PhysicsBody2DComponent(shapes: [.generateBox(width: 0.04, height: 1.5)], mode: .kinematic) - Sprite(tintColor: .white, size: [0.04, 1.5]) - Transform(position: [0, 2.5, 0]) - } - var projectile = PhysicsBody2DComponent(shapes: [.generateBox(width: 0.2, height: 0.2)], mass: 1) - projectile.gravityScale = 0; projectile.fixedRotation = true - state.projectile = world.spawn("CCD box") { - projectile - Sprite(tintColor: Color(1, 0.3, 0.3, 1), size: [0.2, 0.2]) - Transform(position: [-6, 2.5, 0]) - } - state.refireRequested = true - for _ in 0..<4 { - state.markers.append(world.spawn("Contact point") { Sprite(tintColor: Color(1, 0.2, 0.55, 1), size: [0.1, 0.1]); Transform(position: [0, 50, 3]) }) - } -} - -@MainActor -func controlAdvanced(in world: World, session: PhysicsDemoSession) { - let state = session.advanced - do { - if state.dirty { - if let belt = state.belt, belt.components[Collision2DComponent.self]?.material.surfaceSpeed != (state.reverseBelt ? -1.8 : 1.8) { - belt.components[Collision2DComponent.self]?.material = try .generate2D(surfaceSpeed: state.reverseBelt ? -1.8 : 1.8) - } - var mixing = PhysicsMaterialMixing() - mixing.friction = state.lowGrip ? .minimum : .maximum; mixing.priority = 10 - let friction: Float = state.lowGrip ? 0.05 : 0.8 - let rolling: Float = state.rolling ? 0.2 : 0 - if let circle = state.circle, let current = circle.components[PhysicsBody2DComponent.self]?.material, - current.friction != friction || current.rollingResistance != rolling || current.mixing != mixing { - circle.components[PhysicsBody2DComponent.self]?.material = try .generate2D(friction: friction, rollingResistance: rolling, mixing: mixing) - } - var dynamics = PhysicsBody2DDynamics() - dynamics.linearDamping = state.damping ? 2 : 0; dynamics.angularDamping = state.damping ? 3 : 0 - try state.air?.components[PhysicsBody2DComponent.self]?.configureDynamics(dynamics) - var motion = PhysicsBody2DMotion() - motion.lockTranslationX = state.locked; motion.lockTranslationY = state.locked; motion.lockRotation = state.locked - try state.air?.components[PhysicsBody2DComponent.self]?.configureMotion(motion) - state.air?.components[PhysicsWind2DComponent.self]?.settings = .init(velocity: state.windMode == 1 ? [6, 0] : .zero, drag: state.windMode == 0 ? 0 : 1) - if state.windMode == 1 { state.air?.components[PhysicsBody2DComponent.self]?.setAwake(true) } - dynamics = .init(); dynamics.isBullet = state.bullet; dynamics.safetyFactor = 0.01 - try state.projectile?.components[PhysicsBody2DComponent.self]?.configureDynamics(dynamics) - if var solver = state.defaults { - if state.softSolver { - solver.contactHertz = 8; solver.contactDampingRatio = 1; solver.contactPushSpeed = 1 - solver.restitutionIterations = 8; solver.isRestitutionPropagationEnabled = true - } - try world.physicsWorld2D?.configureSolver(solver) - } - state.dirty = false - } - if state.resetRequested { - state.box?.components[PhysicsBody2DComponent.self]?.setPosition([-3, -2.7]) - state.box?.components[PhysicsBody2DComponent.self]?.clearForces() - state.circle?.components[PhysicsBody2DComponent.self]?.setPosition([-6, -2.5]) - state.circle?.components[PhysicsBody2DComponent.self]?.linearVelocity = [2, 0] - state.circle?.components[PhysicsBody2DComponent.self]?.angularVelocity = -2 / 0.6 - state.air?.components[PhysicsBody2DComponent.self]?.setPosition([0, 0.7]) - state.air?.components[PhysicsBody2DComponent.self]?.clearForces() - state.air?.components[PhysicsBody2DComponent.self]?.setAwake(true) - state.refireRequested = true; state.resetRequested = false - } - if state.refireRequested { - state.projectile?.components[PhysicsBody2DComponent.self]?.setPosition([-6, 2.5]) - state.projectile?.components[PhysicsBody2DComponent.self]?.linearVelocity = [50, 0] - state.refireRequested = false - } - if state.sleepRequested { - if let entity = state.air, let body = entity.components[PhysicsBody2DComponent.self] { entity.components[PhysicsBody2DComponent.self]?.setAwake(!body.isAwake) } - state.sleepRequested = false - } - } catch { state.status = "Control failed: \(error)" } -} - -@MainActor -func frameAdvanced(in world: World, session: PhysicsDemoSession) { - let state = session.advanced - guard let physics = world.physicsWorld2D, let box = state.box else { - return - } - let contacts = physics.contacts(for: box) - let points = contacts.flatMap { $0.manifold.points } - state.pointCount = points.count - let impulse = points.reduce(Float(0)) { $0 + $1.totalNormalImpulse } - let separation = points.first?.separation ?? 0 - let identifier = contacts.first.map { String(UInt(bitPattern: $0.id.hashValue), radix: 16).suffix(6) } ?? "none" - let velocity = state.air?.components[PhysicsBody2DComponent.self]?.linearVelocity.x ?? 0 - let awake = state.air?.components[PhysicsBody2DComponent.self]?.isAwake == true - state.status = String( - format: "Contact #%@ · %d points · separation %.3f · impulse %.3f · wind vx %.2f · %@ · hit points %d", - String(identifier), - points.count, - separation, - impulse, - velocity, - awake ? "awake" : "sleeping", - state.hitPoints - ) - for (index, marker) in state.markers.enumerated() { - marker.components[Transform.self]?.position = index < points.count ? Vector3(points[index].point, 3) : [0, 50, 3] - } -} - -struct PhysicsAdvancedDemoPane: View { - let session: PhysicsDemoSession - var body: some View { - let state = session.advanced - VStack(spacing: 8) { - HStack(spacing: 14) { - Button(["Wind: off", "Wind: airflow", "Wind: still-air drag"][state.windMode]) { state.windMode = (state.windMode + 1) % 3; state.dirty = true } - .accessibilityIdentifier("physics-demo.advanced-wind") - Button(state.reverseBelt ? "Belt: left" : "Belt: right") { state.reverseBelt.toggle(); state.dirty = true } - .accessibilityIdentifier("physics-demo.advanced-belt") - Button(state.rolling ? "Rolling resistance: on" : "Rolling resistance: off") { state.rolling.toggle(); state.dirty = true } - .accessibilityIdentifier("physics-demo.advanced-rolling") - Button(state.lowGrip ? "Mixing: minimum grip" : "Mixing: maximum grip") { state.lowGrip.toggle(); state.dirty = true } - .accessibilityIdentifier("physics-demo.advanced-grip") - } - HStack(spacing: 14) { - Button(state.damping ? "Damping: high" : "Damping: off") { state.damping.toggle(); state.dirty = true } - .accessibilityIdentifier("physics-demo.advanced-damping") - Button(state.bullet ? "CCD bullet: on" : "CCD bullet: off") { state.bullet.toggle(); state.dirty = true; state.refireRequested = true } - .accessibilityIdentifier("physics-demo.advanced-bullet") - Button(state.locked ? "Axes: locked" : "Axes: free") { state.locked.toggle(); state.dirty = true } - .accessibilityIdentifier("physics-demo.advanced-lock") - Button("Sleep / wake capsule") { state.sleepRequested = true } - .accessibilityIdentifier("physics-demo.advanced-sleep") - Button(state.softSolver ? "Solver: soft / 8 restitution" : "Solver: default") { state.softSolver.toggle(); state.dirty = true } - .accessibilityIdentifier("physics-demo.advanced-solver") - Button("Reset") { state.resetRequested = true } - .accessibilityIdentifier("physics-demo.advanced-reset") - } - Text("Green: rolling + grip mixing · Cyan: wind + damping · Red: fast box vs thin kinematic wall · Pink: contact points") - Text(state.status).font(.system(size: 12)) - SceneView(useSceneCameras: true, make: { configurePhysicsDemo(&$0, session: session) }, updateContent: { _, _ in }) - .frameCaptureLabel("Physics2D Advanced").id(PhysicsDemoScene.advanced) - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } -} diff --git a/Demos/Physics2D/Physics2DCharactersDemo.swift b/Demos/Physics2D/Physics2DCharactersDemo.swift deleted file mode 100644 index ce8fff37d..000000000 --- a/Demos/Physics2D/Physics2DCharactersDemo.swift +++ /dev/null @@ -1,93 +0,0 @@ -import AdaEngine -import Math - -@MainActor -func setupCharacters(in world: World, session: PhysicsDemoSession) throws { - world.insertResource(PhysicsDebugOptions.showPhysicsShapes) - world.spawn("Floor") { - Sprite(tintColor: Color(0.25, 0.35, 0.5, 1), size: [18, 0.4]) - Collision2DComponent(shapes: [.generateBox(width: 18, height: 0.4)]) - Transform(position: [0, -2.2, 0]) - } - let ramp = try Shape2DResource.generateChain(points: [[7, -1.4], [5, -2], [3, -2]]) - world.spawn("Ramp") { Collision2DComponent(shapes: [ramp]); Transform() } - world.spawn("Right wall") { Collision2DComponent(shapes: [.generateBox(width: 0.2, height: 4)]); Transform(position: [7.7, -0.2, 0]) } - session.sensor = world.spawn("Sensor") { - Sprite(tintColor: Color(0.1, 0.6, 0.4, 0.35), size: [0.5, 2]) - Collision2DComponent(shapes: [.generateBox(width: 0.5, height: 2)], mode: .trigger) - Transform(position: [-4, -1, -1]) - } - var controller = CharacterController2DComponent() - controller.mode = session.characterMode - session.character = world.spawn("Character") { controller; Transform(position: [-5, 0, 1]) } - session.crate = world.spawn("Light crate") { - Sprite(tintColor: Color(1, 0.57, 0.2, 1), size: [0.8, 0.8]) - PhysicsBody2DComponent(shapes: [.generateBox(width: 0.8, height: 0.8)], massProperties: .init(mass: 0.2, inertia: [0, 0, 0.2])) - Transform(position: [-1, -1.6, 0]) - } - var platform = PhysicsBody2DComponent(shapes: [.generateBox(width: 3, height: 0.4)], mass: 1, mode: .kinematic) - platform.linearVelocity = [0.8, 0] - session.platform = world.spawn("Moving platform") { - Sprite(tintColor: Color(0.25, 0.6, 1, 1), size: [3, 0.4]) - platform - Transform(position: [2, -1.3, 0]) - } -} - -@MainActor -func controlCharacter(_ session: PhysicsDemoSession) { - guard let character = session.character else { - return - } - character.components[CharacterController2DComponent.self]?.mode = session.characterMode - character.components[CharacterController2DComponent.self]?.desiredVelocity = [2.5 * session.moveDirection, 0] - if session.jumpRequest { - session.jumpRequest = false - character.components[CharacterController2DComponent.self]?.jumpRequested = true - } - if session.resetRequested { - session.resetRequested = false; session.moveDirection = 0 - character.components[Transform.self]?.position = [-5, 0, 1] - character.components[CharacterController2DComponent.self]?.desiredVelocity = .zero - character.components[CharacterController2DComponent.self]?.resetMotion = true - session.crate?.components[PhysicsBody2DComponent.self]?.setPosition([-1, -1.6], angle: .radians(0)) - session.crate?.components[PhysicsBody2DComponent.self]?.linearVelocity = .zero - session.crate?.components[PhysicsBody2DComponent.self]?.angularVelocity = 0 - } - let x = session.platform?.components[Transform.self]?.position.x ?? 2 - if x > 3.2 { session.platformDirection = -1 } - if x < 1.2 { session.platformDirection = 1 } - session.platform?.components[PhysicsBody2DComponent.self]?.linearVelocity = [0.8 * session.platformDirection, 0] -} - -@MainActor -func updateCharacterStatus(_ session: PhysicsDemoSession) { - guard let controller = session.character?.components[CharacterController2DComponent.self] else { - return - } - let velocity = Vector2((controller.velocity.x * 10).rounded() / 10, (controller.velocity.y * 10).rounded() / 10) - session.rayStatus = "\(controller.mode.rawValue) · \(controller.isGrounded ? "grounded" : "airborne") · velocity \(velocity.x), \(velocity.y)" -} - -struct PhysicsCharactersDemoPane: View { - let session: PhysicsDemoSession - var body: some View { - VStack(spacing: 12) { - HStack(spacing: 14) { - Button("Left") { session.moveDirection = -1 }.accessibilityIdentifier("physics-demo.left") - Button("Stop") { session.moveDirection = 0 }.accessibilityIdentifier("physics-demo.stop") - Button("Right") { session.moveDirection = 1 }.accessibilityIdentifier("physics-demo.right") - Button("Jump") { session.jumpRequest = true }.accessibilityIdentifier("physics-demo.jump") - Button(session.characterMode == .query ? "Mode: Query" : "Mode: Dynamic") { session.characterMode = session.characterMode == .query ? .dynamic : .query } - .accessibilityIdentifier("physics-demo.character-mode") - Button("Reset") { session.resetRequested = true }.accessibilityIdentifier("physics-demo.character-reset") - } - Text("Move and jump onto the moving platform. Dynamic mode pushes the orange crate; query mode is virtual.") - Text(session.rayStatus) - SceneView(useSceneCameras: true, make: { configurePhysicsDemo(&$0, session: session) }, updateContent: { _, _ in }) - .frameCaptureLabel("Physics2D Characters").id(PhysicsDemoScene.characters) - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } -} diff --git a/Demos/Physics2D/Physics2DDemoMain.swift b/Demos/Physics2D/Physics2DDemoMain.swift deleted file mode 100644 index d7497f53c..000000000 --- a/Demos/Physics2D/Physics2DDemoMain.swift +++ /dev/null @@ -1,364 +0,0 @@ -@_spi(Internal) import AdaApp -import AdaEngine -@_spi(Internal) import AdaPlatform -import Foundation - -@main -@MainActor -struct Physics2DDemoMain { - static func main() async throws { - if CommandLine.arguments.contains("--validate") { - try await validate() - return - } - if CommandLine.arguments.contains("--verify") { - Task { @MainActor in - do { - let path = argument(after: "--proof-directory") ?? "/tmp/ada-physics2d-proof" - try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true) - try await verifyWindow(to: path) - report("Physics2DDemo verification PASS") - } catch { - report("Physics2DDemo verification FAIL: \(error)") - } - } - } - try await AppRuntime.run(Physics2DDemoApp()) - } - - private static func argument(after flag: String) -> String? { - guard let index = CommandLine.arguments.firstIndex(of: flag), CommandLine.arguments.indices.contains(index + 1) else { - return nil - } - return CommandLine.arguments[index + 1] - } - - private static func validate() async throws { - for scene in PhysicsDemoScene.allCases { - let session = PhysicsDemoSession(scene: scene, rendering: false) - session.scan = false - let app = AppWorlds(main: World()) - app.addPlugin(MainSchedulerPlugin()).addPlugin(TransformPlugin()) - .addPlugin(Physics2DPlugin(gravity: scene.gravity)) - .addPlugin(PhysicsDemoPlugin(session: session)) - try await app.build() - app.main.insertResource(FixedTime(deltaTime: 1.0 / 60)) - await app.main.runScheduler(.startup) - await advance(app, steps: 240) - if scene == .largeWorld { - try await validateLargeWorld(app, session) - } else if scene == .advanced { - try await validateAdvanced(app, session) - } else if scene == .mechanisms { - guard session.connectedJoints == 9 else { throw Failure.mechanisms } - session.reverseRequested = true - await advance(app, steps: 30) - guard (session.targets[4].components[PhysicsBody2DComponent.self]?.angularVelocity ?? 0) < -1 else { throw Failure.mechanisms } - session.breakRequested = true - await advance(app, steps: 5) - guard session.jointBreaks > 0, session.connectedJoints == 8 else { throw Failure.mechanisms } - session.resetRequested = true - await advance(app, steps: 5) - guard session.connectedJoints == 9 else { throw Failure.mechanisms } - report("PASS: mechanisms · nine joint kinds / live reverse / break / repair") - } else if scene == .characters { - guard session.character?.components[CharacterController2DComponent.self]?.isGrounded == true else { throw Failure.character } - session.characterMode = .dynamic - await advance(app, steps: 90) - session.moveDirection = 1 - await advance(app, steps: 150) - session.moveDirection = 0 - await advance(app, steps: 30) - guard (session.crate?.components[Transform.self]?.position.x ?? -1) > -0.8 else { throw Failure.character } - session.jumpRequest = true - await advance(app, steps: 1) - guard session.character?.components[CharacterController2DComponent.self]?.isGrounded == false else { throw Failure.character } - report("PASS: characters · query ground / dynamic push / jump") - } else if scene == .queries { - guard session.queryEntities.count == 4 else { throw Failure.queries } - session.querySensors = false - await app.main.runScheduler(.postUpdate) - guard session.queryEntities.count == 3 else { throw Failure.queries } - session.probeMode = .overlap - await app.main.runScheduler(.postUpdate) - guard session.queryEntities.count == 1 else { throw Failure.queries } - session.probeMode = .area - await app.main.runScheduler(.postUpdate) - guard session.queryEntities.count == 3 else { throw Failure.queries } - report("PASS: geometry/query scene · cast / sensor exclusion / exact overlap / area") - } else if scene == .raycast { - guard session.lastHits.count == 3, session.lastHits[0].distance < session.lastHits[1].distance else { throw Failure.raycast } - session.allHits = false - await app.main.runScheduler(.postUpdate) - guard session.lastHits.count == 1 else { throw Failure.raycast } - session.allHits = true - session.blueOnly = true - await app.main.runScheduler(.postUpdate) - guard session.lastHits.count == 2 else { throw Failure.raycast } - report("PASS: raycast scene · 3 ordered hits / nearest / 2 blue-layer hits") - } else { - try requireEvents(session) - report("PASS: event scene · contacts=\(session.contactBegins)/\(session.contactEnds), sensors=\(session.sensorBegins)/\(session.sensorEnds), impacts=\(session.impacts)") - } - } - } - - static func advance(_ app: AppWorlds, steps: Int) async { - for _ in 0.. SceneViewFrameSource { - for _ in 0..<100 { - if let source = SceneViewFrameRegistry.latest(label: label), source.frameSequence > 2, source !== previous { - return source - } - try await Task.sleep(for: .milliseconds(100)) - } - report("No ready source for \(label); labels: \(SceneViewFrameRegistry.sources.map(\.label))") - throw Failure.sceneUnavailable - } - - static func nextFrames(_ source: SceneViewFrameSource, count: Int = 5) async throws -> SceneViewCapturedFrame { - var frame = try await source.captureFrame(after: source.frameSequence, timeout: .seconds(10)) - for _ in 1.. 0 && events.contactBegins > 0 && events.contactEnds > 0 && events.impacts > 0 { break } - } - try requireEvents(events) - let eventFrame = try await nextFrames(eventSource) - try write(eventFrame.image, to: directory + "/events.png") - let initialImpacts = events.impacts - try tap("physics-demo.drop") - var observedDrop = false - for _ in 0..<20 { - _ = try await nextFrames(eventSource, count: 1) - if (events.falling?.components[Transform.self]?.position.y ?? 0) > 2 { - observedDrop = true - break - } - } - guard observedDrop else { throw Failure.events } - for _ in 0..<150 { - _ = try await nextFrames(eventSource, count: 1) - if events.impacts > initialImpacts { break } - } - guard events.impacts > initialImpacts else { throw Failure.events } - var verification: [String: Any] = [ - "passed": true, "backend": String(describing: unsafe RenderEngine.shared.type), "raycastAll": 3, "raycastNearest": 1, "raycastBlueMask": 2, - "contactBegins": events.contactBegins, "contactEnds": events.contactEnds, - "sensorBegins": events.sensorBegins, "sensorEnds": events.sensorEnds, "impacts": events.impacts, - "lastImpactSpeed": events.lastImpactSpeed, "sceneReentry": true, "dropButton": true, "rayFrame": rayFrame.sequence, "eventFrame": eventFrame.sequence, - ] - try tap("physics-demo.raycasts") - let rebuiltRay = try await waitForSource("Physics2D Raycasts", excluding: raySource) - _ = try await nextFrames(rebuiltRay) - guard let rebuilt = rebuiltRay.world?.getResource(PhysicsDemoSession.self), - rebuilt.lastHits.count == 3, rebuilt.markers.count == 3 else { throw Failure.raycast } - try tap("physics-demo.events") - _ = try await waitForSource("Physics2D Events", excluding: eventSource) - report("PASS: native events · sensor entry/exit, contact begin/end, impact, re-drop, scene re-entry") - try tap("physics-demo.queries") - let queriesSource = try await waitForSource("Physics2D Geometry & queries") - guard let queries = queriesSource.world?.getResource(PhysicsDemoSession.self) else { throw Failure.sceneUnavailable } - try tap("physics-demo.query-scan") - _ = try await nextFrames(queriesSource) - guard queries.queryEntities.count == 4 else { throw Failure.queries } - try tap("physics-demo.query-sensors") - _ = try await nextFrames(queriesSource) - guard queries.queryEntities.count == 3 else { throw Failure.queries } - try tap("physics-demo.query-mask") - _ = try await nextFrames(queriesSource) - guard queries.queryEntities.count == 2 else { throw Failure.queries } - try tap("physics-demo.query-order") - _ = try await nextFrames(queriesSource) - guard queries.queryEntities.count == 1 else { throw Failure.queries } - try tap("physics-demo.query-order") - try tap("physics-demo.query-mask") - try tap("physics-demo.query-sensors") - let geometryFrame = try await nextFrames(queriesSource) - report("Geometry debug \(queries.debugDrawState), options=\(queriesSource.world?.getResource(PhysicsDebugOptions.self)?.rawValue ?? 0)") - try write(geometryFrame.image, to: directory + "/geometry-cast.png") - try tap("physics-demo.query-kind") - _ = try await nextFrames(queriesSource) - guard queries.queryEntities.count == 1 else { throw Failure.queries } - try tap("physics-demo.query-kind") - let areaFrame = try await nextFrames(queriesSource) - guard queries.queryEntities.count == 4 else { throw Failure.queries } - try write(areaFrame.image, to: directory + "/geometry-area.png") - try tap("physics-demo.query-kind") - try tap("physics-demo.raycasts") - _ = try await waitForSource("Physics2D Raycasts", excluding: rebuiltRay) - try tap("physics-demo.queries") - let rebuiltQueries = try await waitForSource("Physics2D Geometry & queries", excluding: queriesSource) - _ = try await nextFrames(rebuiltQueries) - guard queries.queryEntities.count == 4, queries.targets.count == 8 else { throw Failure.queries } - verification["querySceneReentry"] = true - verification["geometryQueries"] = true - report("PASS: native geometry · convex cast / overlap / area / masks / sensors / nearest") - try tap("physics-demo.mechanisms") - let mechanismSource = try await waitForSource("Physics2D Mechanisms") - guard let mechanisms = mechanismSource.world?.getResource(PhysicsDemoSession.self), mechanisms.connectedJoints == 9 else { throw Failure.mechanisms } - try tap("physics-demo.reverse") - _ = try await nextFrames(mechanismSource, count: 30) - guard mechanisms.driveDirection < 0, (mechanisms.targets[4].components[PhysicsBody2DComponent.self]?.angularVelocity ?? 0) < -1 else { throw Failure.mechanisms } - let mechanismFrame = try await nextFrames(mechanismSource) - try write(mechanismFrame.image, to: directory + "/mechanisms.png") - try tap("physics-demo.break") - for _ in 0..<30 { - _ = try await nextFrames(mechanismSource, count: 1) - if mechanisms.jointBreaks > 0 { break } - } - guard mechanisms.jointBreaks > 0, mechanisms.connectedJoints == 8 else { throw Failure.mechanisms } - try tap("physics-demo.repair") - _ = try await nextFrames(mechanismSource) - guard mechanisms.connectedJoints == 9 else { throw Failure.mechanisms } - verification["mechanisms"] = true - verification["jointBreaks"] = mechanisms.jointBreaks - report("PASS: native mechanisms · nine constraints / reverse / break / repair") - try tap("physics-demo.characters") - let characterSource = try await waitForSource("Physics2D Characters") - guard let characters = characterSource.world?.getResource(PhysicsDemoSession.self) else { throw Failure.character } - try await waitForGround(characterSource, characters) - guard !hasCharacterCollider(characterSource, characters) else { throw Failure.character } - let startX = characters.character?.components[Transform.self]?.position.x ?? -5 - try tap("physics-demo.right") - _ = try await nextFrames(characterSource, count: 25) - guard (characters.character?.components[Transform.self]?.position.x ?? -5) > startX + 0.2 else { throw Failure.character } - try tap("physics-demo.stop") - _ = try await nextFrames(characterSource, count: 15) - try tap("physics-demo.jump") - try await waitForJump(characterSource, characters) - let queryFrame = try await nextFrames(characterSource, count: 10) - try write(queryFrame.image, to: directory + "/character-query.png") - try await waitForGround(characterSource, characters) - try tap("physics-demo.character-mode") - _ = try await nextFrames(characterSource) - guard characters.characterMode == .dynamic, hasCharacterCollider(characterSource, characters) else { throw Failure.character } - try tap("physics-demo.right") - _ = try await nextFrames(characterSource, count: 120) - try tap("physics-demo.stop") - _ = try await nextFrames(characterSource, count: 15) - guard (characters.crate?.components[Transform.self]?.position.x ?? -1) > -0.8 else { throw Failure.character } - let dynamicFrame = try await nextFrames(characterSource) - try write(dynamicFrame.image, to: directory + "/character-dynamic.png") - try tap("physics-demo.jump") - try await waitForJump(characterSource, characters) - try await waitForGround(characterSource, characters) - try tap("physics-demo.character-mode") - _ = try await nextFrames(characterSource) - guard !hasCharacterCollider(characterSource, characters) else { throw Failure.character } - let oldPlayer = characters.character - try tap("physics-demo.mechanisms") - _ = try await waitForSource("Physics2D Mechanisms", excluding: mechanismSource) - try tap("physics-demo.characters") - let rebuiltCharacter = try await waitForSource("Physics2D Characters", excluding: characterSource) - try await waitForGround(rebuiltCharacter, characters) - guard characters.character !== oldPlayer else { throw Failure.character } - verification["characterModes"] = ["query", "dynamic"] - verification["dynamicPush"] = true - verification["characterJumpBothModes"] = true - verification["characterColliderCleanup"] = true - verification["characterSceneReentry"] = true - report("PASS: native characters · move / stop / both jumps / physical push / mode cleanup / re-entry") - try await verifyAdvanced(to: directory, tap: tap) - verification["advancedPhysics"] = true - try await verifyLargeWorld(to: directory, tap: tap) - verification["largeWorldPrecision"] = true - verification["largeWorldMillimeterNudge"] = true - verification["largeWorldOriginShift"] = true - verification["largeWorldCharacters"] = true - verification["largeWorldSceneReentry"] = true - try JSONSerialization.data(withJSONObject: verification, options: [.prettyPrinted, .sortedKeys]) - .write(to: URL(fileURLWithPath: directory + "/verification.json"), options: .atomic) - } - - static func waitForGround(_ source: SceneViewFrameSource, _ session: PhysicsDemoSession) async throws { - for _ in 0..<150 { - if session.character?.components[CharacterController2DComponent.self]?.isGrounded == true { - return - } - _ = try await nextFrames(source, count: 1) - } - throw Failure.character - } - - static func waitForJump(_ source: SceneViewFrameSource, _ session: PhysicsDemoSession) async throws { - for _ in 0..<30 { - _ = try await nextFrames(source, count: 1) - if session.character?.components[CharacterController2DComponent.self]?.isGrounded == false { - return - } - } - throw Failure.character - } - - static func hasCharacterCollider(_ source: SceneViewFrameSource, _ session: PhysicsDemoSession) -> Bool { - guard let player = session.character, let bottom = player.components[Transform.self]?.position.xy, let physics = source.world?.physicsWorld2D else { - return false - } - return physics.overlap(at: bottom + [0, 0.9]).contains { $0.entity === player } - } - - private static func requireEvents(_ session: PhysicsDemoSession) throws { - guard session.contactBegins > 0, session.contactEnds > 0, session.sensorBegins > 0, - session.sensorEnds > 0, session.impacts > 0, session.lastImpactSpeed > 0 else { throw Failure.events } - } - - static func write(_ image: Image, to path: String) throws { - guard image.width > 0, image.height > 0, !image.data.isEmpty else { throw Failure.emptyFrame } - var bytes = Array(image.data) - if image.format == .bgra8 || image.format == .bgra8_sRGB { - for index in stride(from: 0, to: bytes.count, by: 4) { bytes.swapAt(index, index + 2) } - } - var colors = Set() - for offset in stride(from: 0, to: bytes.count - 3, by: 64) { - colors.insert(UInt32(bytes[offset]) << 16 | UInt32(bytes[offset + 1]) << 8 | UInt32(bytes[offset + 2])) - } - let png = try PNGWriter().encode(width: image.width, height: image.height, rgba: bytes) - try png.write(to: URL(fileURLWithPath: path), options: .atomic) - guard image.width > 200, image.height > 200, colors.count > 3 else { report("Frame \(path) contains \(colors.count) sampled colors"); throw Failure.emptyFrame } - } - - static func report(_ message: String) { - FileHandle.standardOutput.write(Data((message + "\n").utf8)) - } - - enum Failure: Error { - case raycast, events, queries, mechanisms, character, advanced, largeWorld, sceneUnavailable, emptyFrame - } -} diff --git a/Demos/Physics2D/Physics2DDemoScenes.swift b/Demos/Physics2D/Physics2DDemoScenes.swift deleted file mode 100644 index f902f0dca..000000000 --- a/Demos/Physics2D/Physics2DDemoScenes.swift +++ /dev/null @@ -1,302 +0,0 @@ -import AdaEngine -import Math -import Observation - -/// The same scenes run in the interactive window, offscreen capture and deterministic validation. -enum PhysicsDemoScene: String, CaseIterable { - case raycast, events, queries, mechanisms, characters, advanced, largeWorld - var gravity: Vector2 { self == .raycast || self == .queries ? .zero : [0, -9.81] } -} - -@MainActor -@Observable -final class PhysicsDemoSession: Resource { - let scene: PhysicsDemoScene - let rendering: Bool - var largeWorld = PhysicsLargeWorldState() - var advanced = PhysicsAdvancedState() - var mechanismOwners: [Entity] = [] - var mechanismStarts: [Vector2] = [] - var reverseRequested = false - var breakRequested = false - var driveDirection: Float = 1 - var moverDirection: Float = 1 - var connectedJoints = 0 - var jointBreaks = 0 - var character: Entity? - var crate: Entity? - var platform: Entity? - var characterMode = CharacterController2DMode.query - var moveDirection: Float = 0 - var jumpRequest = false - var platformDirection: Float = 1 - var debugDrawState = "extract pending" - var probeMode = PhysicsProbeMode.cast - var querySensors = true - var queryShape: Shape2DResource? - var queryEntities: [Entity] = [] - var queryPoints: [Vector2] = [] - var probeY: Float = 1.6 - var probeContact: Vector2? - var allHits = true - var blueOnly = false - var scan = true - var elapsed: Float = 0 - var resetRequested = false - var rayStatus = "Waiting for physics sync" - var contactBegins = 0 - var contactEnds = 0 - var sensorBegins = 0 - var sensorEnds = 0 - var impacts = 0 - var lastImpactSpeed: Float = 0 - var sensorOccupied = false - var targets: [Entity] = [] - var markers: [Entity] = [] - var beam: Entity? - var sensor: Entity? - var visitor: Entity? - var falling: Entity? - var lastHits: [Raycast2DHit] = [] - - func resetScene() { - largeWorld = PhysicsLargeWorldState() - advanced = PhysicsAdvancedState() - elapsed = 0 - mechanismOwners.removeAll(); mechanismStarts.removeAll() - reverseRequested = false; breakRequested = false; driveDirection = 1; moverDirection = 1 - connectedJoints = 0; jointBreaks = 0 - character = nil; crate = nil; platform = nil; moveDirection = 0; jumpRequest = false; platformDirection = 1 - resetRequested = false - rayStatus = "Waiting for physics sync" - contactBegins = 0 - contactEnds = 0 - sensorBegins = 0 - sensorEnds = 0 - impacts = 0 - lastImpactSpeed = 0 - sensorOccupied = false - targets.removeAll() - markers.removeAll() - beam = nil - sensor = nil - visitor = nil - falling = nil - lastHits.removeAll() - queryEntities.removeAll(); queryPoints.removeAll(); queryShape = nil; probeContact = nil - } - - init(scene: PhysicsDemoScene, rendering: Bool = true) { - self.scene = scene - self.rendering = rendering - } -} - -@MainActor -func configurePhysicsDemo(_ app: inout AppWorlds, session: PhysicsDemoSession) { - app.addPlugin(TransformPlugin()).addPlugin(InputPlugin()).addPlugin(RenderWorldPlugin()) - .addPlugin(CameraPlugin()).addPlugin(AssetsPlugin()).addPlugin(VisibilityPlugin()) - .addPlugin(SpritePlugin()).addPlugin(Core2DPlugin()).addPlugin(UpscalePlugin()) - .addPlugin(Physics2DPlugin(gravity: session.scene.gravity)) - .addPlugin(PhysicsDemoPlugin(session: session)) -} - -struct PhysicsDemoPlugin: Plugin { - let session: PhysicsDemoSession - - func setup(in app: AppWorlds) { - app.insertResource(session) - .addSystem(PhysicsDemoStartupSystem.self, on: .startup) - .addSystem(PhysicsDemoControlsSystem.self, on: .fixedPreUpdate) - .addSystem(PhysicsDemoFrameSystem.self, on: .postUpdate) - app.getSubworldBuilder(by: .renderWorld)?.addSystem(PhysicsDemoProbeExtractSystem.self, on: .extract) - } -} - -@System -@MainActor -func PhysicsDemoStartup(_ context: WorldUpdateContext, _ state: Res) { - let session = state.wrappedValue - session.resetScene() - let world = context.world - if session.rendering { - var camera = Camera() - camera.backgroundColor = Color(0.035, 0.055, 0.10, 1) - let entity = world.spawn("Physics camera", bundle: Camera2D(camera: camera, orthographicProjection: OrthographicProjection(scale: 48))) - if session.scene == .largeWorld { entity.components[PhysicsPosition2DComponent.self] = .init(session.largeWorld.base) } - } - if session.scene == .largeWorld { - do { try setupLargeWorld(in: world, session: session) } catch { session.rayStatus = "Large-world setup failed: \(error)" } - return - } - if session.scene == .advanced { - do { try setupAdvanced(in: world, session: session) } catch { session.rayStatus = "Advanced setup failed: \(error)" } - return - } - if session.scene == .mechanisms || session.scene == .characters { - do { - if session.scene == .mechanisms { try setupMechanisms(in: world, session: session) } else { try setupCharacters(in: world, session: session) } - } catch { session.rayStatus = "Setup failed: \(error)" } - return - } - if session.scene == .queries { - do { try setupGeometryQueries(in: world, session: session) } catch { session.rayStatus = "Geometry failed: \(error)" } - return - } - if session.scene == .raycast { - for (index, x) in [Float(-3), 0, 3].enumerated() { - let color: Color = index == 1 ? Color(1, 0.57, 0.20, 1) : Color(0.25, 0.55, 1, 1) - let entity = world.spawn("Target \(index + 1)") { - Sprite(tintColor: color, size: [0.8, 3]) - Collision2DComponent( - shapes: [.generateBox(width: 0.8, height: 3)], - filter: CollisionFilter(categoryBitMask: CollisionGroup(rawValue: index == 1 ? 2 : 1)) - ) - Transform(position: [x, 0, 0]) - } - session.targets.append(entity) - } - session.beam = world.spawn("Ray") { - Sprite(tintColor: Color(0.3, 1, 0.8, 1), size: [12, 0.035]) - Transform(position: [0, 0, 1]) - } - for _ in 0..<3 { - session.markers.append(world.spawn("Hit marker") { - Sprite(tintColor: .white, size: [0.16, 0.16]) - Transform(position: [0, 50, 2]) - }) - } - } else { - world.spawn("Floor") { - Sprite(tintColor: Color(0.3, 0.4, 0.55, 1), size: [14, 0.5]) - Collision2DComponent(shapes: [.generateBox(width: 14, height: 0.5)]) - Transform(position: [0, -2, 0]) - } - session.sensor = world.spawn("Sensor") { - Sprite(tintColor: Color(0.15, 0.6, 0.4, 0.45), size: [3, 3]) - Collision2DComponent(shapes: [.generateBox(width: 3, height: 3)], mode: .trigger) - Transform(position: [-2, 0.5, -1]) - } - var visitorBody = PhysicsBody2DComponent(shapes: [.generateBox(width: 0.5, height: 0.5)], mass: 1) - visitorBody.gravityScale = 0 - visitorBody.linearVelocity = [2.5, 0] - session.visitor = world.spawn("Sensor visitor") { - Sprite(tintColor: Color(0.3, 0.75, 1, 1), size: [0.5, 0.5]) - visitorBody - Transform(position: [-5, 0.5, 1]) - } - session.falling = world.spawn("Bouncing box") { - Sprite(tintColor: Color(1, 0.57, 0.2, 1), size: [0.8, 0.8]) - PhysicsBody2DComponent( - shapes: [.generateBox(width: 0.8, height: 0.8)], - mass: 1, - material: .generate(friction: 0.4, restitution: 0.65, density: 1) - ) - Transform(position: [3, 3, 1]) - } - } -} - -@System -@MainActor -func PhysicsDemoControls(_ context: WorldUpdateContext, _ state: Res, _ time: Res) { - let session = state.wrappedValue - session.elapsed += Float(time.deltaTime) - if session.scene == .largeWorld { - controlLargeWorld(in: context.world, session: session) - return - } - if session.scene == .advanced { - controlAdvanced(in: context.world, session: session) - return - } - if session.scene == .mechanisms { - controlMechanisms(session) - return - } - if session.scene == .characters { - controlCharacter(session) - return - } - guard session.scene == .events else { - return - } - if let visitor = session.visitor, (visitor.components[Transform.self]?.position.x ?? 0) > 1.5 { - visitor.components[PhysicsBody2DComponent.self]?.setPosition([-5, 0.5]) - visitor.components[PhysicsBody2DComponent.self]?.linearVelocity = [2.5, 0] - } - if session.resetRequested { - session.resetRequested = false - session.falling?.components[PhysicsBody2DComponent.self]?.setPosition([3, 3]) - session.falling?.components[PhysicsBody2DComponent.self]?.linearVelocity = [0, -0.1] - } -} - -@System -@MainActor -func PhysicsDemoFrame( - _ context: WorldUpdateContext, - _ state: Res, - _ begins: Events, - _ ends: Events, - _ hits: Events, - _ jointEvents: Events -) { - let session = state.wrappedValue - for event in begins { - if event.isSensor { session.sensorBegins += 1; session.sensorOccupied = true } else { session.contactBegins += 1 } - } - for event in ends { - if event.isSensor { session.sensorEnds += 1; session.sensorOccupied = false } else { session.contactEnds += 1 } - } - for _ in jointEvents { session.jointBreaks += 1 } - for event in hits { - session.impacts += 1; session.lastImpactSpeed = event.approachSpeed - if session.scene == .advanced { session.advanced.hitPoints = event.contact?.manifold.points.count ?? 0 } - } - session.sensor?.components[Sprite.self]?.tintColor = session.sensorOccupied ? Color(0.2, 1, 0.5, 0.8) : Color(0.15, 0.6, 0.4, 0.45) - if session.scene == .largeWorld { - frameLargeWorld(in: context.world, session: session) - return - } - if session.scene == .advanced { - frameAdvanced(in: context.world, session: session) - return - } - if session.scene == .mechanisms { - session.connectedJoints = session.mechanismOwners.filter { $0.components[PhysicsJoint2DComponent.self]?.isConnected == true }.count - session.rayStatus = "\(session.connectedJoints) / 9 connected · \(session.jointBreaks) break events · filter box passes through its block" - return - } - if session.scene == .characters { - updateCharacterStatus(session) - return - } - if session.scene == .queries { - updateGeometryQueries(in: context.world, session: session) - return - } - guard session.scene == .raycast, let physics = context.world.physicsWorld2D else { - return - } - let start = Vector2(-6, 0) - let end = Vector2(6, session.scan ? Math.sin(session.elapsed * 0.7) * 3 : 0) - let hits = physics.raycast( - from: start, - to: end, - query: session.allHits ? .all : .first, - mask: session.blueOnly ? .default : .all - ) - session.lastHits = hits - let description = hits.map { "\($0.entity.name): \((($0.distance * 100).rounded()) / 100) m" }.joined(separator: " · ") - session.rayStatus = hits.isEmpty ? "No hits" : description - let translation = end - start - session.beam?.components[Sprite.self]?.size = Size(width: translation.squaredLength.squareRoot(), height: 0.035) - session.beam?.components[Transform.self] = Transform( - rotation: Quat(axis: [0, 0, 1], angle: Math.atan2(translation.y, translation.x)), - position: Vector3((start + end) / 2, 1) - ) - for (index, marker) in session.markers.enumerated() { - marker.components[Transform.self]?.position = index < hits.count ? Vector3(hits[index].point, 2) : [0, 50, 2] - } -} diff --git a/Demos/Physics2D/Physics2DDemoView.swift b/Demos/Physics2D/Physics2DDemoView.swift deleted file mode 100644 index 849a59b73..000000000 --- a/Demos/Physics2D/Physics2DDemoView.swift +++ /dev/null @@ -1,70 +0,0 @@ -import AdaEngine -import Observation - -struct Physics2DDemoApp: App { - var body: some AppScene { - WindowGroup { Physics2DDemoView() } - .window(with: UIWindow.Configuration(title: "AdaEngine · Box2D playground", frame: Rect(x: 100, y: 100, width: 1000, height: 700))) - } -} - -private struct Physics2DDemoView: View { - @State private var scene = PhysicsDemoScene.raycast - @State private var raycast = PhysicsDemoSession(scene: .raycast) - @State private var events = PhysicsDemoSession(scene: .events) - @State private var queries = PhysicsDemoSession(scene: .queries) - @State private var mechanisms = PhysicsDemoSession(scene: .mechanisms) - @State private var characters = PhysicsDemoSession(scene: .characters) - @State private var advanced = PhysicsDemoSession(scene: .advanced) - @State private var largeWorld = PhysicsDemoSession(scene: .largeWorld) - - var body: some View { - VStack(spacing: 12) { - Text("Box2D playground").font(.system(size: 26)) - HStack(spacing: 20) { - Button("Raycasts") { scene = .raycast }.accessibilityIdentifier("physics-demo.raycasts") - Button("Collisions & sensors") { scene = .events }.accessibilityIdentifier("physics-demo.events") - Button("Geometry & queries") { scene = .queries }.accessibilityIdentifier("physics-demo.queries") - Button("Mechanisms") { scene = .mechanisms }.accessibilityIdentifier("physics-demo.mechanisms") - Button("Characters") { scene = .characters }.accessibilityIdentifier("physics-demo.characters") - Button("Advanced") { scene = .advanced }.accessibilityIdentifier("physics-demo.advanced") - Button("Large worlds") { scene = .largeWorld }.accessibilityIdentifier("physics-demo.large-world") - } - if scene == .raycast { - HStack(spacing: 18) { - Button(raycast.allHits ? "All hits" : "Nearest hit") { raycast.allHits.toggle() }.accessibilityIdentifier("physics-demo.mode") - Button(raycast.blueOnly ? "Blue targets only" : "All layers") { raycast.blueOnly.toggle() }.accessibilityIdentifier("physics-demo.mask") - Button(raycast.scan ? "Hold horizontal ray" : "Animate ray") { raycast.scan.toggle() }.accessibilityIdentifier("physics-demo.scan") - } - Text("White markers show hits in distance order. Toggle the orange layer or nearest hit.") - Text(raycast.rayStatus) - SceneView(useSceneCameras: true, make: { configurePhysicsDemo(&$0, session: raycast) }, updateContent: { _, _ in }) - .frameCaptureLabel("Physics2D Raycasts") - .id(PhysicsDemoScene.raycast) - .frame(maxWidth: .infinity, maxHeight: .infinity) - } else if scene == .largeWorld { - PhysicsLargeWorldDemoPane(session: largeWorld) - } else if scene == .advanced { - PhysicsAdvancedDemoPane(session: advanced) - } else if scene == .mechanisms { - PhysicsMechanismsDemoPane(session: mechanisms) - } else if scene == .characters { - PhysicsCharactersDemoPane(session: characters) - } else if scene == .queries { - PhysicsQueriesDemoPane(session: queries) - } else { - Button("Drop box again") { events.resetRequested = true }.accessibilityIdentifier("physics-demo.drop") - Text("Blue box crosses the green sensor freely. Orange box bounces on the floor.") - Text("Contacts: \(events.contactBegins) begin / \(events.contactEnds) end · Impacts: \(events.impacts)") - Text("Sensor: \(events.sensorBegins) enter / \(events.sensorEnds) exit · \(events.sensorOccupied ? "occupied" : "empty")") - SceneView(useSceneCameras: true, make: { configurePhysicsDemo(&$0, session: events) }, updateContent: { _, _ in }) - .frameCaptureLabel("Physics2D Events") - .id(PhysicsDemoScene.events) - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - } - .padding(18) - .background(Color(0.035, 0.055, 0.10, 1)) - .foregroundColor(.white) - } -} diff --git a/Demos/Physics2D/Physics2DLargeWorldChecks.swift b/Demos/Physics2D/Physics2DLargeWorldChecks.swift deleted file mode 100644 index b08bb88a6..000000000 --- a/Demos/Physics2D/Physics2DLargeWorldChecks.swift +++ /dev/null @@ -1,86 +0,0 @@ -import AdaEngine - -extension Physics2DDemoMain { - static func validateLargeWorld(_ app: AppWorlds, _ session: PhysicsDemoSession) async throws { - let state = session.largeWorld - guard let physics = app.main.physicsWorld2D, let player = session.character, let hit = state.hit, - state.pointCount > 0, state.hinge?.components[PhysicsJoint2DComponent.self]?.isConnected == true else { throw Failure.largeWorld } - let before = hit.x - state.nudgeRequested = true - await advance(app, steps: 2) - guard let nudged = state.hit, nudged.x - before > 0.0009, nudged.x - before < 0.0011 else { throw Failure.largeWorld } - let contact = state.contactID - state.shifted = true - await advance(app, steps: 2) - guard physics.coordinateOrigin == state.base.offset(by: [1000, -500]), state.hit == nudged, - state.contactID == contact, player.components[CharacterController2DComponent.self]?.isGrounded == true else { throw Failure.largeWorld } - let start = player.components[PhysicsPosition2DComponent.self]?.position.x ?? 0 - state.direction = 1 - await advance(app, steps: 30) - state.direction = 0 - await advance(app, steps: 15) - guard (player.components[PhysicsPosition2DComponent.self]?.position.x ?? 0) > start + 0.1 else { throw Failure.largeWorld } - state.jumpRequested = true - await advance(app, steps: 1) - guard player.components[CharacterController2DComponent.self]?.isGrounded == false else { throw Failure.largeWorld } - await advance(app, steps: 90) - state.mode = .dynamic - await advance(app, steps: 60) - guard player.components[CharacterController2DComponent.self]?.isGrounded == true else { throw Failure.largeWorld } - state.jumpRequested = true - await advance(app, steps: 1) - guard player.components[CharacterController2DComponent.self]?.isGrounded == false else { throw Failure.largeWorld } - report("PASS: large worlds · exact queries / 1 mm nudge / origin shift / stable contacts and hinge / both character modes") - } - - static func verifyLargeWorld(to directory: String, tap: (String) throws -> Void) async throws { - let oldAdvanced = SceneViewFrameRegistry.latest(label: "Physics2D Advanced") - try tap("physics-demo.large-world") - let source = try await waitForSource("Physics2D Large worlds") - guard let session = source.world?.getResource(PhysicsDemoSession.self), let physics = source.world?.physicsWorld2D, - let player = session.character else { throw Failure.largeWorld } - let state = session.largeWorld - try await waitForGround(source, session) - guard let hit = state.hit, state.pointCount > 0 else { throw Failure.largeWorld } - let contact = state.contactID - try tap("physics-demo.large-nudge") - _ = try await nextFrames(source) - guard let nudged = state.hit, nudged.x - hit.x > 0.0009, nudged.x - hit.x < 0.0011 else { throw Failure.largeWorld } - let before = try await nextFrames(source) - try write(before.image, to: directory + "/large-world-before.png") - try tap("physics-demo.large-origin") - let shifted = try await nextFrames(source) - guard physics.coordinateOrigin == state.base.offset(by: [1000, -500]), state.hit == nudged, - state.contactID == contact, state.hinge?.components[PhysicsJoint2DComponent.self]?.isConnected == true else { throw Failure.largeWorld } - try write(shifted.image, to: directory + "/large-world-shifted.png") - let start = player.components[PhysicsPosition2DComponent.self]?.position.x ?? 0 - try tap("physics-demo.large-right") - _ = try await nextFrames(source, count: 30) - try tap("physics-demo.large-stop") - _ = try await nextFrames(source, count: 15) - guard (player.components[PhysicsPosition2DComponent.self]?.position.x ?? 0) > start + 0.1 else { throw Failure.largeWorld } - try tap("physics-demo.large-jump") - try await waitForJump(source, session) - try await waitForGround(source, session) - try tap("physics-demo.large-mode") - _ = try await nextFrames(source) - try await waitForGround(source, session) - guard hasCharacterCollider(source, session) else { throw Failure.largeWorld } - let dynamic = try await nextFrames(source) - try write(dynamic.image, to: directory + "/large-world-dynamic.png") - try tap("physics-demo.large-jump") - try await waitForJump(source, session) - try await waitForGround(source, session) - try tap("physics-demo.large-mode") - _ = try await nextFrames(source) - guard !hasCharacterCollider(source, session) else { throw Failure.largeWorld } - try tap("physics-demo.advanced") - _ = try await waitForSource("Physics2D Advanced", excluding: oldAdvanced) - try tap("physics-demo.large-world") - let rebuilt = try await waitForSource("Physics2D Large worlds", excluding: source) - guard let fresh = rebuilt.world?.getResource(PhysicsDemoSession.self), fresh.character !== player, - let freshPhysics = rebuilt.world?.physicsWorld2D, freshPhysics.coordinateOrigin == state.base else { throw Failure.largeWorld } - if let contact { guard !freshPhysics.isValid(contact) else { throw Failure.largeWorld } } - report("PASS: native large worlds · exact query / millimeter nudge / camera origin shift / contacts / both character jumps / re-entry") - } -} diff --git a/Demos/Physics2D/Physics2DLargeWorldDemo.swift b/Demos/Physics2D/Physics2DLargeWorldDemo.swift deleted file mode 100644 index 1bc754149..000000000 --- a/Demos/Physics2D/Physics2DLargeWorldDemo.swift +++ /dev/null @@ -1,152 +0,0 @@ -import AdaEngine -import Foundation -import Math -import Observation - -@MainActor -@Observable -final class PhysicsLargeWorldState { - let base = PhysicsPosition2D(x: 1_000_000_000.125, y: -999_999_999.75) - var shifted = false - var nudgeRequested = false - var mode = CharacterController2DMode.query - var direction: Float = 0 - var jumpRequested = false - var target: Entity? - var crate: Entity? - var hinge: Entity? - var marker: Entity? - var hit: PhysicsPosition2D? - var distance: Float = 0 - var probeStart = Vector2.zero - var probeEnd = Vector2.zero - var hitLocal: Vector2? - var contactID: Contact2DID? - var pointCount = 0 - var status = "Waiting for precise queries" -} - -@MainActor -func setupLargeWorld(in world: World, session: PhysicsDemoSession) throws { - let state = session.largeWorld - guard let physics = world.physicsWorld2D, physics.setCoordinateOrigin(state.base) else { - return - } - world.insertResource(PhysicsDebugOptions([.showPhysicsShapes, .showJoints])) - world.spawn("Global floor") { - Collision2DComponent(shapes: [.generateBox(width: 16, height: 0.4)]) - Sprite(tintColor: Color(0.25, 0.35, 0.5, 1), size: [16, 0.4]) - Transform() - PhysicsPosition2DComponent(state.base.offset(by: [0, -2.5])) - } - for x: Float in [-8, 8] { - world.spawn("Global boundary") { - Collision2DComponent(shapes: [.generateBox(width: 0.2, height: 4)]) - Transform() - PhysicsPosition2DComponent(state.base.offset(by: [x, -0.3])) - } - } - state.target = world.spawn("Millimeter query target") { - Collision2DComponent(shapes: [.generateBox(width: 0.4, height: 1.2)], filter: CollisionFilter(categoryBitMask: CollisionGroup(rawValue: 8))) - Sprite(tintColor: Color(0.2, 0.65, 1, 1), size: [0.4, 1.2]) - Transform() - PhysicsPosition2DComponent(state.base.offset(by: [3, 0.8])) - } - state.crate = world.spawn("Global contact box") { - PhysicsBody2DComponent(shapes: [.generateBox(width: 0.8, height: 0.8)], massProperties: .init(mass: 1, inertia: [0, 0, 1])) - Sprite(tintColor: Color(1, 0.6, 0.2, 1), size: [0.8, 0.8]) - Transform() - PhysicsPosition2DComponent(state.base.offset(by: [-1, -1.9])) - } - var bob = PhysicsBody2DComponent(shapes: [.generateCircle(radius: 0.8)], massProperties: .init(mass: 1, inertia: [0, 0, 0.2])) - bob.debugColor = Color(0.5, 1, 0.65, 1); bob.angularVelocity = 1 - let pendulum = world.spawn("Global hinge bob") { bob; Transform(); PhysicsPosition2DComponent(state.base.offset(by: [-3, 0.3])) } - let descriptor = try PhysicsJoint2DDescriptor.revolute(entity: pendulum, globalAnchor: state.base.offset(by: [-3, 2])) - state.hinge = world.spawn("Exact global pivot") { PhysicsJoint2DComponent(joint: descriptor) } - let controller = CharacterController2DComponent(radius: 0.3, height: 1.5) - session.character = world.spawn("Global character") { controller; Transform(); PhysicsPosition2DComponent(state.base.offset(by: [1, 0])) } - state.marker = world.spawn("Precise hit marker") { - Sprite(tintColor: Color(1, 0.85, 0.2, 1), size: [0.12, 0.12]) - Transform(position: [0, 0, 2]) - PhysicsPosition2DComponent(state.base.offset(by: [3, 0.8])) - } -} - -@MainActor -func controlLargeWorld(in world: World, session: PhysicsDemoSession) { - let state = session.largeWorld - let origin = state.shifted ? state.base.offset(by: [1000, -500]) : state.base - if world.physicsWorld2D?.coordinateOrigin != origin { world.physicsWorld2D?.setCoordinateOrigin(origin) } - if state.nudgeRequested, let target = state.target, let position = target.components[PhysicsPosition2DComponent.self]?.position { - target.components[PhysicsPosition2DComponent.self]?.position = .init(x: position.x + 0.001, y: position.y) - state.nudgeRequested = false - } - if let player = session.character { - if player.components[CharacterController2DComponent.self]?.mode != state.mode { - player.components[CharacterController2DComponent.self]?.mode = state.mode - player.components[CharacterController2DComponent.self]?.resetMotion = true - } - player.components[CharacterController2DComponent.self]?.desiredVelocity = [state.direction * 2, 0] - if state.jumpRequested { player.components[CharacterController2DComponent.self]?.jumpRequested = true; state.jumpRequested = false } - } -} - -@MainActor -func frameLargeWorld(in world: World, session: PhysicsDemoSession) { - let state = session.largeWorld - guard let physics = world.physicsWorld2D, let target = state.target, - let targetPosition = target.components[PhysicsPosition2DComponent.self]?.position else { - return - } - let start = state.base.offset(by: [-6, 0.8]) - let hit = physics.raycast(from: start, translation: [12, 0], query: .first, mask: CollisionGroup(rawValue: 8)).first - state.hit = hit?.point; state.distance = hit?.distance ?? 0 - state.probeStart = start.displacement(from: physics.coordinateOrigin) - state.probeEnd = start.offset(by: [12, 0]).displacement(from: physics.coordinateOrigin) - state.hitLocal = hit?.point.displacement(from: physics.coordinateOrigin) - if let marker = state.marker { - let position = hit?.point ?? state.base.offset(by: [0, 50]) - marker.components[PhysicsPosition2DComponent.self]?.position = position - marker.components[Transform.self]?.position = Vector3(position.displacement(from: physics.coordinateOrigin), 2) - } - let contact = state.crate.flatMap { physics.contacts(for: $0).first } - state.contactID = contact?.id; state.pointCount = contact?.manifold.points.count ?? 0 - let delta = targetPosition.x - state.base.x - 3 - let grounded = session.character?.components[CharacterController2DComponent.self]?.isGrounded == true - state.status = String( - format: "Global hit x %.6f · travel %.3f · target delta %.6f · %d contact points · %@ %@", - hit?.point.x ?? 0, - state.distance, - delta, - state.pointCount, - state.mode.rawValue, - grounded ? "grounded" : "airborne" - ) -} - -struct PhysicsLargeWorldDemoPane: View { - let session: PhysicsDemoSession - var body: some View { - let state = session.largeWorld - VStack(spacing: 10) { - HStack(spacing: 14) { - Button("Left") { state.direction = -1 }.accessibilityIdentifier("physics-demo.large-left") - Button("Stop") { state.direction = 0 }.accessibilityIdentifier("physics-demo.large-stop") - Button("Right") { state.direction = 1 }.accessibilityIdentifier("physics-demo.large-right") - Button("Jump") { state.jumpRequested = true }.accessibilityIdentifier("physics-demo.large-jump") - Button(state.mode == .query ? "Mode: Query" : "Mode: Dynamic") { state.mode = state.mode == .query ? .dynamic : .query } - .accessibilityIdentifier("physics-demo.large-mode") - Button(state.shifted ? "Restore render origin" : "Shift render origin 1 km") { state.shifted.toggle() } - .accessibilityIdentifier("physics-demo.large-origin") - Button("Nudge target 1 mm") { state.nudgeRequested = true }.accessibilityIdentifier("physics-demo.large-nudge") - } - Text("Exact physics at 1,000,000,000 units · Camera and tagged roots share the render frame · Yellow probe targets the blue pillar") - .font(.system(size: 12)) - Text(state.status).font(.system(size: 12)) - SceneView(useSceneCameras: true, make: { configurePhysicsDemo(&$0, session: session) }, updateContent: { _, _ in }) - .frameCaptureLabel("Physics2D Large worlds").id(PhysicsDemoScene.largeWorld) - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } -} diff --git a/Demos/Physics2D/Physics2DMechanismsDemo.swift b/Demos/Physics2D/Physics2DMechanismsDemo.swift deleted file mode 100644 index 122199d98..000000000 --- a/Demos/Physics2D/Physics2DMechanismsDemo.swift +++ /dev/null @@ -1,132 +0,0 @@ -import AdaEngine -import Math - -@MainActor -func demoHinge(_ direction: Float) -> RevoluteJoint2DSettings { - .init(frameB: .init(position: [-0.7, 1]), limits: .init(lower: -0.7, upper: 0.7), motor: .init(speed: direction, maximumEffort: 20)) -} -@MainActor -func demoSlider(_ direction: Float) -> PrismaticJoint2DSettings { - .init(spring: .init(hertz: 2), limits: .init(lower: -0.6, upper: 0.6), motor: .init(speed: direction, maximumEffort: 80)) -} -@MainActor -func demoWheel(_ direction: Float) -> WheelJoint2DSettings { - .init(frameA: .init(angle: .pi / 2), limits: .init(lower: -0.8, upper: 0.25), motor: .init(speed: 3 * direction, maximumEffort: 5)) -} -@MainActor -func demoMotor(_ direction: Float) -> MotorJoint2DSettings { - .init(frameA: .init(position: [0.7, 0]), angularVelocity: direction, maximumVelocityForce: 0, maximumVelocityTorque: 3, linearSpring: .init(hertz: 2)) -} - -@MainActor -func setupMechanisms(in world: World, session: PhysicsDemoSession) throws { - world.insertResource(PhysicsDebugOptions([.showPhysicsShapes, .showJoints])) - world.physicsWorld2D?.isSleepEnabled = false - let positions: [(Vector2, Vector2)] = [ - ([-6, 2.6], [-5, 1.6]), ([0, 2.6], [0.7, 1.6]), ([5, 2.2], [5, 2.2]), - ([-6, 0.1], [-5, 0.1]), ([0, 0.1], [0, -0.4]), ([5, 0.1], [5, 0.1]), - ([-6, -2.2], [-6.7, -2.2]), ([0, -2.2], [0, -2.2]), ([5, -2.6], [5, -1.8]) - ] - for (index, pair) in positions.enumerated() { - let anchorShape = index == 6 ? Shape2DResource.generateBox(width: 1.2, height: 0.8) : .generateCircle(radius: 0.18) - var anchorBody = PhysicsBody2DComponent(shapes: [anchorShape], mass: 1, mode: .static) - anchorBody.debugColor = Color(0.65, 0.7, 0.8, 1) - let a = world.spawn("Anchor \(index)") { anchorBody; Transform(position: Vector3(pair.0, 0)) } - let shape: Shape2DResource - if index == 4 { - shape = .generateCircle(radius: 0.8) - } else if index == 8 { - shape = try .generateCapsule(from: [0, -0.25], to: [0, 0.25], radius: 0.18) - } else { - shape = .generateBox(width: 0.45, height: 0.45) - } - var body = PhysicsBody2DComponent(shapes: [shape], massProperties: .init(mass: 1, inertia: [0, 0, 0.1])) - body.debugColor = index == 0 ? Color(1, 0.57, 0.2, 1) : Color(0.3, 0.7, 1, 1) - if index == 6 { body.gravityScale = 0; body.linearVelocity = [0.5, 0] } - if index == 3 { body.linearVelocity = [0, 1]; body.angularVelocity = 1 } - let b = world.spawn("Mechanism body \(index)") { body; Transform(position: Vector3(pair.1, 0)) } - let descriptor: PhysicsJoint2DDescriptor - switch index { - case 0: descriptor = try PhysicsJoint2DDescriptor.rope(entityA: a, entityB: b).withBreakSettings(.init(forceThreshold: 40)) - case 1: descriptor = try .revolute(entityA: a, entityB: b, settings: demoHinge(1)) - case 2: descriptor = try .prismatic(entityA: a, entityB: b, settings: demoSlider(1)) - case 3: - descriptor = try .weld( - entityA: a, - entityB: b, - settings: .init(frameA: .init(position: [1, 0]), linearSpring: .init(hertz: 2, dampingRatio: 0.3), angularSpring: .init(hertz: 2)) - ) - case 4: descriptor = try .wheel(entityA: a, entityB: b, settings: demoWheel(1)) - case 5: descriptor = try .motor(entityA: a, entityB: b, settings: demoMotor(1)) - case 6: descriptor = try .filter(entityA: a, entityB: b) - case 7: - descriptor = try .mover(entityA: a, entityB: b, settings: .init(linearVelocity: [1, 0], maximumVelocityForce: [20, 20], collideConnected: false)) - default: descriptor = try .pogo(entityA: a, entityB: b, settings: .init(restLength: 0.8, maximumCompressionForce: 100)) - } - session.mechanismOwners.append(world.spawn("Joint \(index)") { PhysicsJoint2DComponent(joint: descriptor) }) - session.targets.append(b) - session.mechanismStarts.append(pair.1) - } -} - -@MainActor -func controlMechanisms(_ session: PhysicsDemoSession) { - guard session.targets.count == 9, session.mechanismOwners.count == 9 else { - return - } - if session.resetRequested { - session.resetRequested = false - for (index, entity) in session.targets.enumerated() { - entity.components[PhysicsBody2DComponent.self]?.setPosition(session.mechanismStarts[index], angle: .radians(0)) - entity.components[PhysicsBody2DComponent.self]?.linearVelocity = index == 6 ? [0.5, 0] : .zero - entity.components[PhysicsBody2DComponent.self]?.angularVelocity = 0 - session.mechanismOwners[index].components[PhysicsJoint2DComponent.self]?.reconnect() - } - return - } - if session.breakRequested { - session.breakRequested = false - session.targets[0].components[PhysicsBody2DComponent.self]?.applyForceToCenter([0, -5000], wake: true) - } - do { - var retuneMover = false - if session.reverseRequested { - session.reverseRequested = false; session.driveDirection *= -1; session.moverDirection *= -1 - try session.mechanismOwners[1].components[PhysicsJoint2DComponent.self]?.updateSettings(demoHinge(session.driveDirection)) - try session.mechanismOwners[2].components[PhysicsJoint2DComponent.self]?.updateSettings(demoSlider(session.driveDirection)) - try session.mechanismOwners[4].components[PhysicsJoint2DComponent.self]?.updateSettings(demoWheel(session.driveDirection)) - try session.mechanismOwners[5].components[PhysicsJoint2DComponent.self]?.updateSettings(demoMotor(session.driveDirection)) - retuneMover = true - } - let x = session.targets[7].components[Transform.self]?.position.x ?? 0 - if (x > 0.8 && session.moverDirection > 0) || (x < -0.8 && session.moverDirection < 0) { session.moverDirection *= -1; retuneMover = true } - if retuneMover { - try session.mechanismOwners[7].components[PhysicsJoint2DComponent.self]?.updateSettings( - MoverJoint2DSettings(linearVelocity: [session.moverDirection, 0], maximumVelocityForce: [20, 20], collideConnected: false) - ) - } - } catch { session.rayStatus = "Tuning failed: \(error)" } - let filter = session.targets[6] - let x = filter.components[Transform.self]?.position.x ?? -6 - let velocity = filter.components[PhysicsBody2DComponent.self]?.linearVelocity.x ?? 0.5 - if (x > -5.2 && velocity > 0) || (x < -6.8 && velocity < 0) { filter.components[PhysicsBody2DComponent.self]?.linearVelocity = [-velocity, 0] } -} - -struct PhysicsMechanismsDemoPane: View { - let session: PhysicsDemoSession - var body: some View { - VStack(spacing: 12) { - HStack(spacing: 18) { - Button("Reverse motors") { session.reverseRequested = true }.accessibilityIdentifier("physics-demo.reverse") - Button("Break rope") { session.breakRequested = true }.accessibilityIdentifier("physics-demo.break") - Button("Repair all") { session.resetRequested = true }.accessibilityIdentifier("physics-demo.repair") - } - Text("Top: rope / hinge / slider · middle: weld / wheel / motor · bottom: filter / mover / pogo") - Text(session.rayStatus) - SceneView(useSceneCameras: true, make: { configurePhysicsDemo(&$0, session: session) }, updateContent: { _, _ in }) - .frameCaptureLabel("Physics2D Mechanisms").id(PhysicsDemoScene.mechanisms) - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } -} diff --git a/Demos/Physics2D/Physics2DQueriesDemo.swift b/Demos/Physics2D/Physics2DQueriesDemo.swift deleted file mode 100644 index b277210d1..000000000 --- a/Demos/Physics2D/Physics2DQueriesDemo.swift +++ /dev/null @@ -1,135 +0,0 @@ -import AdaEngine -import Math - -@MainActor -enum PhysicsProbeMode: String, CaseIterable { - case cast = "Shape cast", overlap = "Capsule overlap", area = "Area candidates" - var next: Self { self == .cast ? .overlap : self == .overlap ? .area : .cast } -} - -@MainActor -func setupGeometryQueries(in world: World, session: PhysicsDemoSession) throws { - world.insertResource(PhysicsDebugOptions.showPhysicsShapes) - func add(_ name: String, _ shape: Shape2DResource, at position: Vector2 = .zero, layer: UInt64 = 1, sensor: Bool = false) { - var collision = Collision2DComponent(shapes: [shape], mode: sensor ? .trigger : .default, filter: .init(categoryBitMask: CollisionGroup(rawValue: layer))) - collision.debugColor = layer == 2 ? Color(1, 0.57, 0.2, 1) : sensor ? Color(0.2, 0.9, 0.5, 1) : Color(0.3, 0.65, 1, 1) - session.targets.append(world.spawn(name) { collision; Transform(position: Vector3(position, 0)) }) - } - add("Rounded box", try .generateRoundedBox(width: 1.8, height: 1.2, radius: 0.25), at: [-4, 1.6], layer: 2) - add("Capsule", try .generateCapsule(from: [0, -0.6], to: [0, 0.6], radius: 0.45), at: [0, 1.6]) - add("Sensor", .generateBox(width: 0.6, height: 2), at: [-2, 1.6], sensor: true) - add("Segment", try .generateSegment(from: [-0.8, -0.6], to: [0.8, 0.6]), at: [3.7, 1.6]) - add("Rounded triangle", try .generateRoundedPolygon(vertices: [[-0.6, -0.5], [0.6, -0.5], [0, 0.6]], radius: 0.15), at: [1.5, -0.6], layer: 2) - add("Open chain", try .generateChain(points: [[6, -1.6], [3, -2], [-1, -1.3], [-6, -2]])) - add("Loop", try .generateChain(points: [[-0.7, -0.5], [0.7, -0.5], [0.7, 0.5], [-0.7, 0.5]], isLoop: true), at: [5, -0.5]) - add("Loose chain segment", try .generateChainSegment(from: [1, 0], to: [-1, 0], ghostStart: [2, 0], ghostEnd: [-2, 0]), at: [-4, -0.6]) - session.queryShape = try .generateCapsule(from: [0, -0.25], to: [0, 0.25], radius: 0.2) -} - -@MainActor -func updateGeometryQueries(in world: World, session: PhysicsDemoSession) { - guard let physics = world.physicsWorld2D, let shape = session.queryShape else { - return - } - session.probeY = 1.6 + (session.scan ? Math.sin(session.elapsed * 0.7) : 0) - session.probeContact = nil - do { - let mask: CollisionGroup = session.blueOnly ? .default : .all - switch session.probeMode { - case .cast: - let hits = try physics.shapeCast( - shape, - from: Transform(position: [-7, session.probeY, 0]), - to: [7, session.probeY], - query: session.allHits ? .all : .first, - mask: mask, - includeSensors: session.querySensors - ) - session.queryEntities = hits.map(\.entity) - session.queryPoints = hits.map(\.point) - session.probeContact = hits.first.map { [-7 + $0.distance, session.probeY] } - case .overlap: - session.queryEntities = try physics.overlap(shape, at: Transform(position: [0, session.probeY, 0]), mask: mask, includeSensors: session.querySensors).map(\.entity) - session.queryPoints = session.queryEntities.compactMap { $0.components[Transform.self]?.position.xy } - case .area: - session.queryEntities = physics.overlap(in: Rect(x: -6, y: 0.5, width: 12, height: 2.2), mask: mask, includeSensors: session.querySensors).map(\.entity) - session.queryPoints = session.queryEntities.compactMap { $0.components[Transform.self]?.position.xy } - } - session.rayStatus = "\(session.probeMode.rawValue) · \(session.queryEntities.count) hits: " + session.queryEntities.map(\.name).joined(separator: " · ") - } catch { - session.queryEntities.removeAll(); session.queryPoints.removeAll() - session.rayStatus = "Query failed: \(error)" - } -} - -@System(dependencies: [.after(ExtractPhysicsDebugSystem.self)]) -@MainActor -func PhysicsDemoProbeExtract( - _ state: Extract>, - _ shapes: ResMut, - _ data: Res, - _ sorted: Res?> -) { - let session = state.wrappedValue.wrappedValue - if session.scene == .largeWorld { - let state = session.largeWorld - shapes.wrappedValue.addLine(from: state.probeStart, to: state.probeEnd, color: Color(1, 0.8, 0.2, 1)) - } - if session.scene == .characters || session.scene == .largeWorld { - if let character = session.character, let controller = character.components[CharacterController2DComponent.self], - controller.mode == .query, let bottom = character.components[Transform.self]?.position.xy { - shapes.wrappedValue.addCapsule( - from: bottom + [0, controller.settings.radius], - to: bottom + [0, controller.settings.height - controller.settings.radius], - radius: controller.settings.radius, - color: .white - ) - } - return - } - guard session.scene == .queries else { - return - } - if session.probeMode == .area { - let corners: [Vector2] = [[-6, 0.5], [6, 0.5], [6, 2.7], [-6, 2.7]] - for index in 0..<4 { shapes.wrappedValue.addLine(from: corners[index], to: corners[(index + 1) % 4], color: .white) } - } else { - let x: Float = session.probeMode == .cast ? -7 : 0 - shapes.wrappedValue.addCapsule(from: [x, session.probeY - 0.25], to: [x, session.probeY + 0.25], radius: 0.2, color: .white) - if session.probeMode == .cast { - shapes.wrappedValue.addLine(from: [-7, session.probeY], to: [7, session.probeY], color: Color(0.5, 0.6, 0.7, 1)) - if let contact = session.probeContact { - shapes.wrappedValue.addCapsule(from: contact - [0, 0.25], to: contact + [0, 0.25], radius: 0.2, color: Color(1, 0.85, 0.3, 1)) - } - } - } - session.debugDrawState = "lines=\(shapes.lines.count), vertices=\(data.wrappedValue?.lineVertexBuffer.count ?? -1), items=\(sorted.wrappedValue?.items.items.count ?? -1)" - for point in session.queryPoints { - shapes.wrappedValue.addLine(from: point - [0.08, 0], to: point + [0.08, 0], color: .white) - shapes.wrappedValue.addLine(from: point - [0, 0.08], to: point + [0, 0.08], color: .white) - } -} - -struct PhysicsQueriesDemoPane: View { - let session: PhysicsDemoSession - var body: some View { - VStack(spacing: 12) { - HStack(spacing: 14) { - Button(session.probeMode.rawValue) { session.probeMode = session.probeMode.next }.accessibilityIdentifier("physics-demo.query-kind") - Button(session.querySensors ? "Include sensors" : "Solids only") { session.querySensors.toggle() }.accessibilityIdentifier("physics-demo.query-sensors") - Button(session.blueOnly ? "Layer 1 only" : "All layers") { session.blueOnly.toggle() }.accessibilityIdentifier("physics-demo.query-mask") - Button(session.scan ? "Hold probe" : "Animate probe") { session.scan.toggle() }.accessibilityIdentifier("physics-demo.query-scan") - if session.probeMode == .cast { - Button(session.allHits ? "All hits" : "Nearest hit") { session.allHits.toggle() }.accessibilityIdentifier("physics-demo.query-order") - } - } - Text("White probe queries colored colliders. Area mode shows broad-phase AABB candidates.") - Text(session.rayStatus) - SceneView(useSceneCameras: true, make: { configurePhysicsDemo(&$0, session: session) }, updateContent: { _, _ in }) - .frameCaptureLabel("Physics2D Geometry & queries") - .id(PhysicsDemoScene.queries) - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } -} diff --git a/Demos/Physics2D/README.md b/Demos/Physics2D/README.md deleted file mode 100644 index 93c09f13e..000000000 --- a/Demos/Physics2D/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# Box2D playground - -Five interactive AdaEngine scenes, using the same physics systems as games: - -- **Raycasts:** a scanning ray, distance-ordered white hit markers, nearest/all - selection and a blue-only collision mask. The orange block is on another layer. -- **Collisions & sensors:** an orange box bounces on a floor while a blue box - crosses a green sensor without a physical response. Live counters distinguish - solid contacts, sensor enter/exit and impact events delivered through ECS. - “Drop box again” repeats the impact. - -- **Geometry & queries:** capsule, rounded box/polygon, segment, open/closed chains - and a loose chain segment. A white capsule sweeps or overlaps the colored - colliders; area mode draws broad-phase candidates. Toggle sensor inclusion, - layer mask, nearest/all and animation. The outlines follow native geometry. - -- **Mechanisms:** a 3x3 grid of rope, hinge, slider, weld, suspension wheel, - pose/velocity motor, pair filter, mover and pogo rigs. Reverse live motors, - overload the rope to emit a break event, and repair the connections. Pogo is - experimental in upstream Box2D 3.2. -- **Characters:** left/stop/right, jump, reset and query/dynamic mode controls. - The query capsule is virtual; the native dynamic capsule pushes the orange - crate. A moving platform and ramp exercise support and slope behavior. - -On macOS run `./Demos/Physics2D/script/build_and_run.sh`. Use `--verify` to launch -all scenes, exercise query/character/motor controls, check live event counters and -save real GPU frames plus `verification.json` in `Demos/Physics2D/dist/proof/`. - -Verification also checks both jumps, physical crate movement, native collider -cleanup after a mode switch, repair, and scene re-entry. GPU frames include -`geometry-cast.png`, `geometry-area.png`, `mechanisms.png`, -`character-query.png`, and `character-dynamic.png`. - -Deterministic simulation without a window: - -```sh -swift run --scratch-path /tmp/adaengine-box2d-3.2-build Physics2DDemo --validate -``` - -## Query and event semantics - -`raycast(from:to:)` casts a finite segment and returns distance-sorted hits. -`.first` means nearest. Invalid/zero-length input and misses return an empty -array. Initial overlaps are ignored. `raycast(from: Ray, maxDistance:)` uses -its origin and normalized XY direction; the default range is 10,000 world units. -Masks select shape categories; the native Box2D query filter also applies. - -`CollisionEvents.Began` / `.Ended` include `isSensor`; for sensors `entityA` -is the sensor and the impulse is zero. `.Hit` contains the impact point, normal, -approach speed and the contact's total normal impulse for the current step. -Native shape pairs produce separate events, including compound bodies. -End events whose entities/shapes have already been destroyed are skipped. -A fast visitor swept through a sensor emits begin in that step and end in the next. - -Subscribe through `world.subscribe(to:)` for immediate delivery. `Events` -receives the same events through the world-local ECS buffer on the next frame. -The physics plugin installs the event lifecycle if it has not already been set up. - -The Advanced tab exercises live body settings, native CCD, conveyors, rolling and material mixing, wind/still-air drag, contact point snapshots and solver presets. `--validate` and native `--verify` include these controls; the latter writes `proof/advanced-physics.png` from the actual scene camera. - -The Large worlds tab demonstrates exact Double physics at 1e9, a one-millimeter target nudge, camera/render-origin shifts, stable contacts/global hinges and both character modes. Native `--verify` captures before/after origin frames and the dynamic character, and checks scene re-entry without stale contact handles. diff --git a/Demos/Physics2D/script/build_and_run.sh b/Demos/Physics2D/script/build_and_run.sh deleted file mode 100755 index 7815ab117..000000000 --- a/Demos/Physics2D/script/build_and_run.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -MODE="${1:-run}" -DEMO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -REPO_ROOT="$(cd "$DEMO_ROOT/../.." && pwd)" -BUILD_PATH="${ADAENGINE_PHYSICS2D_BUILD_PATH:-/tmp/adaengine-box2d-3.2-build}" -mkdir -p "$BUILD_PATH" -APP="$DEMO_ROOT/dist/Physics2DDemo.app" -export ADAENGINE_DISABLE_SWAN=1 -export ADAENGINE_SKIP_SWIFTLINT=1 -case "$MODE" in run|--verify|--debug|--logs|--telemetry) ;; *) echo "usage: $0 [run|--verify|--debug|--logs|--telemetry]" >&2; exit 2 ;; esac -for demo_pid in $(pgrep -f "$APP/Contents/MacOS/Physics2DDemo" || true); do - demo_command="$(ps -p "$demo_pid" -o command= || true)" - if [[ "$demo_command" == "$APP/Contents/MacOS/Physics2DDemo" || "$demo_command" == "$APP/Contents/MacOS/Physics2DDemo "* ]]; then - kill "$demo_pid" - fi -done -mkdir -p "$DEMO_ROOT/dist" -if ! swift build --package-path "$REPO_ROOT" --scratch-path "$BUILD_PATH" --disable-sandbox --product Physics2DDemo > "$DEMO_ROOT/dist/build.log" 2>&1; then - tail -60 "$DEMO_ROOT/dist/build.log" - exit 1 -fi -BIN="$(swift build --package-path "$REPO_ROOT" --scratch-path "$BUILD_PATH" --disable-sandbox --show-bin-path)" -mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" -cp "$BIN/Physics2DDemo" "$APP/Contents/MacOS/" -for resource in "$BIN"/*.bundle; do - [[ -d "$resource" ]] && cp -R "$resource" "$APP/" -done -cat > "$APP/Contents/Info.plist" <<'PLIST' - - - -CFBundleExecutablePhysics2DDemo -CFBundleIdentifierorg.adaengine.demo.physics2d -CFBundleNameAdaEngine Box2D playground -CFBundlePackageTypeAPPL -LSMinimumSystemVersion15.0 -NSPrincipalClassNSApplication - -PLIST -: > "$DEMO_ROOT/dist/runtime.log" -rm -f "$DEMO_ROOT/dist/proof/verification.json" -if [[ "$MODE" == --debug ]]; then - /usr/bin/lldb -- "$APP/Contents/MacOS/Physics2DDemo" - exit -fi -OPEN_ARGS=(-n "$APP" --stdout "$DEMO_ROOT/dist/runtime.log" --stderr "$DEMO_ROOT/dist/runtime.log" --args) -if [[ "$MODE" == --verify ]]; then OPEN_ARGS+=(--verify --proof-directory "$DEMO_ROOT/dist/proof"); fi -/usr/bin/open "${OPEN_ARGS[@]}" -case "$MODE" in - --verify) - for attempt in {1..60}; do - if rg -q 'Physics2DDemo verification PASS' "$DEMO_ROOT/dist/runtime.log"; then - cat "$DEMO_ROOT/dist/runtime.log" - exit 0 - fi - if rg -q 'verification FAIL|Fatal error|BOX2D ASSERTION' "$DEMO_ROOT/dist/runtime.log"; then - tail -40 "$DEMO_ROOT/dist/runtime.log" - exit 1 - fi - sleep 1 - done - tail -40 "$DEMO_ROOT/dist/runtime.log" - echo 'Physics2DDemo verification timed out' >&2 - exit 1 - ;; - --logs|--telemetry) /usr/bin/log stream --info --style compact --predicate 'process == "Physics2DDemo"' ;; -esac diff --git a/Demos/SpineNative/Package.resolved b/Demos/SpineNative/Package.resolved deleted file mode 100644 index 833c88964..000000000 --- a/Demos/SpineNative/Package.resolved +++ /dev/null @@ -1,150 +0,0 @@ -{ - "originHash" : "666c6e07655e3b6d48cd4f5fa7c3184f6951b995cbbdccf534d1b53696f76b50", - "pins" : [ - { - "identity" : "javascriptkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftwasm/JavaScriptKit.git", - "state" : { - "revision" : "67f3be5cc54fa8134cbccafd9c1a3026dd6b5750", - "version" : "0.60.0" - } - }, - { - "identity" : "swan", - "kind" : "remoteSourceControl", - "location" : "https://github.com/adobe/swan", - "state" : { - "revision" : "1eb45a22c79439b50e9cde774454450e50147149", - "version" : "0.0.8" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", - "state" : { - "revision" : "6a52f3251125d74daf04fcbd5e6f08a75d074382", - "version" : "1.8.2" - } - }, - { - "identity" : "swift-atomics", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-atomics", - "state" : { - "revision" : "0442cb5a3f98ab802acb777929fdb446bda11a34", - "version" : "1.3.1" - } - }, - { - "identity" : "swift-cmark", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-cmark.git", - "state" : { - "revision" : "08ddb528923cc1a6527e02b7a1aee9e516ca749a", - "version" : "0.9.0" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "35a1edcb8ebd2bfe251d587f0b69e62af28f508c", - "version" : "1.7.3" - } - }, - { - "identity" : "swift-distributed-tracing", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-distributed-tracing", - "state" : { - "revision" : "297b5b9c82a5631e3199ce3ec0993a3cfb0fbb84", - "version" : "1.7.2" - } - }, - { - "identity" : "swift-format", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-format.git", - "state" : { - "revision" : "62eaad2822b865407b8cde56c36386c00800f7ec", - "version" : "602.0.0" - } - }, - { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log.git", - "state" : { - "revision" : "4038b6a4f74a1426f47eb1dd5b4bb5628a15d597", - "version" : "1.16.1" - } - }, - { - "identity" : "swift-markdown", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-markdown.git", - "state" : { - "revision" : "25cb61d3482054b09ae76ca4f281b1bfe7fe5a43", - "version" : "0.9.0" - } - }, - { - "identity" : "swift-service-context", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-service-context", - "state" : { - "revision" : "853ba3d9ce78851b9cc20108e8bd4f13f7fa546c", - "version" : "1.5.1" - } - }, - { - "identity" : "swift-subprocess", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-subprocess.git", - "state" : { - "branch" : "0.2.1", - "revision" : "44922dfe46380cd354ca4b0208e717a3e92b13dd" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-syntax", - "state" : { - "revision" : "4799286537280063c85a32f09884cfbca301b1a1", - "version" : "602.0.0" - } - }, - { - "identity" : "swift-system", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-system", - "state" : { - "revision" : "869129b7bf4ecc57b97d0193ad29690ca2134750", - "version" : "1.8.1" - } - }, - { - "identity" : "yams", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SpectralDragon/Yams.git", - "state" : { - "branch" : "fb676da", - "revision" : "fb676da5b91f677ab8e8d06b7fcde45b25181531" - } - }, - { - "identity" : "zlib", - "kind" : "remoteSourceControl", - "location" : "https://github.com/the-swift-collective/zlib.git", - "state" : { - "revision" : "f1d153b90420f9fcc6ef916cd67ea96f0e68d137", - "version" : "1.3.2" - } - } - ], - "version" : 3 -} \ No newline at end of file diff --git a/Demos/SpineNative/Package.swift b/Demos/SpineNative/Package.swift deleted file mode 100644 index dda2673ba..000000000 --- a/Demos/SpineNative/Package.swift +++ /dev/null @@ -1,27 +0,0 @@ -// swift-tools-version: 6.2 -import Foundation -import PackageDescription - -let mcpPath = ProcessInfo.processInfo.environment["ADA_MCP_LOCAL_PATH"] -let package = Package( - name: "SpineNative", - platforms: [.macOS(.v15)], - dependencies: [ - .package(name: "AdaEngine", path: "../.."), - mcpPath.map { .package(name: "AdaMCP", path: $0) } - ?? .package(url: "https://github.com/AdaEngine/AdaMCP.git", revision: "v0.2.0"), - ], - targets: [ - .executableTarget( - name: "SpineNative", - dependencies: [ - .product(name: "AdaEngine", package: "AdaEngine"), - .product(name: "AdaSpine", package: "AdaEngine"), - .product(name: "AdaMCPPlugin", package: "AdaMCP"), - ], - path: ".", - exclude: ["README.md", "capture.py"], - sources: ["SpineNativeApp.swift"] - ), - ] -) diff --git a/Demos/SpineNative/README.md b/Demos/SpineNative/README.md deleted file mode 100644 index f0c8829e8..000000000 --- a/Demos/SpineNative/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Native Spine demo - -This separate Swift package runs `SpinePlugin` in an AdaEngine native window. -It uses the licensed example assets beside `../SpinePreview/Assets`. - -Run from the engine checkout: - -``` -swift run --package-path Demos/SpineNative SpineNative -``` - -On Windows use the native toolchain/dependencies prepared by -`script/build_studio_windows.ps1`. Set `TINT_EXECUTABLE` to the verified Tint -binary and `ADA_MCP_LOCAL_PATH` to the Windows AdaMCP package prepared by -`script/ensure_windows_mcp.py`. `ADAENGINE_PACKAGE_PATH` must point to this -engine checkout, and `ADAENGINE_GRAVITY_PACKAGE_PATH` can select the prepared -Windows Gravity package. Do not enable `ADAENGINE_HEADLESS`. - -Windows capture requires the AdaMCP compatibility package with asynchronous -GPU readback; unpatched AdaMCP 0.2.0 uses the unsupported synchronous texture -accessor. The compatible MCP holds exclusive world access during capture, since a -native frame must not replace the target while its readback is pending. - -AdaMCP listens at `http://127.0.0.1:2511/mcp` so Studio can keep port 2510. -Discovery is disabled because the endpoint is explicit. -Use `spine.pose` (`animation`, `time`, `playing`) followed by -`render.capture_screenshot` for repeatable captures from the actual engine GPU -output. `runtime.resume` resumes the engine after capture pauses it. - -With the app running, capture three poses and optionally a walk sequence: - -``` -python Demos/SpineNative/capture.py --frames 30 -``` - -The capture report includes backend/render-graph metadata, camera, native -dimensions, original GPU capture paths and exact pose times. - -Validated on Windows with NVIDIA RTX 4070 / D3D12: native 1000×760 PNGs for -walk, run and jump, followed by 32-frame and 90-frame capture sequences. These -use the actual Spine render systems, GPU textures and AdaMCP capture service. diff --git a/Demos/SpineNative/SpineNativeApp.swift b/Demos/SpineNative/SpineNativeApp.swift deleted file mode 100644 index 0e573700f..000000000 --- a/Demos/SpineNative/SpineNativeApp.swift +++ /dev/null @@ -1,141 +0,0 @@ -import AdaEngine -import AdaMCPCore -import AdaMCPPlugin -import AdaSpine -import Foundation -import MCP - -/// Runs the production Spine render systems in a native window, with AdaMCP GPU capture. -@main -struct SpineNativeApp: App { - var body: some AppScene { - DefaultAppWindow() - .addPlugins(SpinePlugin(), SpineDemoPlugin()) - .window(with: UIWindow.Configuration( - title: "AdaEngine · Spine 4.2 · native GPU", - frame: Rect(x: 100, y: 80, width: 1000, height: 760) - )) - } -} - -private struct SpineDemoPlugin: Plugin { - func setup(in app: borrowing AppWorlds) { - app.addSystem(StartupSpineDemoSystem.self, on: .startup) - let worlds = copy app - app.addPlugin(MCPPlugin(configuration: .init( - port: 2511, - serverName: "AdaEngine Spine native demo", - additionalTools: { - [Tool( - name: "spine.pose", - description: "Select a Spine animation and exact time, optionally continuing playback.", - inputSchema: .object([ - "type": "object", - "properties": .object([ - "animation": .object(["type": "string"]), - "time": .object(["type": "number", "minimum": 0]), - "playing": .object(["type": "boolean"]), - ]), - "required": ["animation"], - ]) - )] - }, - additionalToolHandler: { name, arguments in - guard name == "spine.pose" else { return nil } - do { - let message = try await worlds.withWorldAccess { - try selectSpinePose(in: worlds.main, arguments: arguments) - } - return .init(content: [.text(text: message, annotations: nil, _meta: nil)]) - } catch { - return .init(content: [.text(text: String(describing: error), annotations: nil, _meta: nil)], isError: true) - } - }, - enableDiscovery: false - ))) - } -} - -@MainActor -private func selectSpinePose(in world: World, arguments: [String: Value]) throws -> String { - guard case let .string(clip)? = arguments["animation"], - let entity = world.getEntities().first(where: { $0.components[SpineAnimation.self] != nil }), - var animation = entity.components[SpineAnimation.self] else { - throw SpineError.invalidData("Spine character is not ready.") - } - let time: Float - switch arguments["time"] { - case let .double(value): time = Float(value) - case let .int(value): time = Float(value) - default: time = 0 - } - guard time.isFinite, time >= 0, animation.player.animationNames.contains(clip) else { - throw SpineError.invalidData("Select an existing animation and a finite, non-negative time.") - } - animation.player.resetToSetupPose() - try animation.player.play(clip, repeats: clip != "jump", transitionDuration: 0) - animation.player.advance(by: time) - animation.isPaused = arguments["playing"] != .bool(true) - entity.components[SpineAnimation.self] = animation - let vertices = animation.player.snapshot().flatMap(\.vertices) - if let minX = vertices.map(\.x).min(), let maxX = vertices.map(\.x).max(), - let minY = vertices.map(\.y).min(), let maxY = vertices.map(\.y).max(), - let owner = entity.components[Transform.self], - let cameraEntity = world.getEntities().first(where: { $0.components[Camera.self] != nil }), - var camera = cameraEntity.components[Camera.self] { - camera.projection = .custom(SpineDemoProjection(center: [ - owner.position.x + (minX + maxX) / 2, owner.position.y + (minY + maxY) / 2, - ])) - cameraEntity.components[Camera.self] = camera - } - return "\(clip) at \(time)s; bones=\(animation.player.boneSnapshot().count)" -} - -@System -@MainActor -func StartupSpineDemo(_ context: WorldUpdateContext) async { - do { - let demoDirectory = URL(fileURLWithPath: #filePath).deletingLastPathComponent() - let assets = demoDirectory.appendingPathComponent("../SpinePreview/Assets").standardizedFileURL - let image = try await AssetsManager.load(Image.self, at: assets.appendingPathComponent("spineboy.png").path) - guard let sourceImage = image.asset else { throw SpineError.invalidData("Atlas image is not loaded.") } - let texture = AssetHandle(Texture2D(image: sourceImage, samplerDescription: .init(minFilter: .linear, magFilter: .linear))) - let skeleton = try SpineSkeleton( - skeleton: Data(contentsOf: assets.appendingPathComponent("spineboy-pro.skel")), - atlas: String(contentsOf: assets.appendingPathComponent("spineboy.atlas"), encoding: .utf8), - textures: [texture], - format: .binary - ) - let animation = try SpineAnimation(skeleton: skeleton) - try animation.player.play("walk", transitionDuration: 0) - context.world.spawn("Spineboy") { - animation - Transform(position: [-50, -280, 0]) - NoFrustumCulling() - } - var camera = Camera() - camera.projection = .custom(SpineDemoProjection()) - camera.backgroundColor = Color(0.035, 0.055, 0.10, 1) - context.world.spawn("Spine Camera") { - camera - Transform() - GlobalViewUniform() - VisibleEntities() - Visibility.visible - CameraRenderGraph(subgraphLabel: .main2D, inputSlot: Main2DRenderNode.InputNode.view) - } - print("[SpineNative] Ready: native renderer; AdaMCP http://127.0.0.1:2511/mcp") - } catch { - print("[SpineNative] Failed to load character: \(error)") - } -} - -private struct SpineDemoProjection: CameraProjection { - var center: Vector2 = [15, 60] - var near: Float { -1 } - var far: Float { 1000 } - func makeClipView() -> Transform3D { - Transform3D(scale: [2 / 1000, 2 / 760, 0.001]) * Transform3D(translation: [-center.x, -center.y, 0]) - } - mutating func updateView(width: Float, height: Float) {} -} diff --git a/Demos/SpineNative/capture.py b/Demos/SpineNative/capture.py deleted file mode 100644 index c96f4fbe6..000000000 --- a/Demos/SpineNative/capture.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Capture native engine pixels through AdaMCP, with no browser renderer.""" -import argparse -import json -from pathlib import Path -import shutil -from time import sleep -import urllib.request - - -class MCP: - def __init__(self, endpoint): - self.endpoint = endpoint - self.session = None - self.sequence = 0 - self.request("initialize", { - "protocolVersion": "2024-11-05", "capabilities": {}, - "clientInfo": {"name": "SpineNative capture", "version": "1.0"}, - }) - - def request(self, method, params): - self.sequence += 1 - headers = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} - if self.session: - headers["Mcp-Session-Id"] = self.session - request = urllib.request.Request(self.endpoint, headers=headers, data=json.dumps({ - "jsonrpc": "2.0", "id": self.sequence, "method": method, "params": params, - }).encode()) - with urllib.request.urlopen(request, timeout=60) as response: - self.session = response.headers.get("Mcp-Session-Id", self.session) - body = response.read().decode() - if body.startswith("event:") or body.startswith("data:"): - body = next(line[5:].strip() for line in body.splitlines() if line.startswith("data:")) - reply = json.loads(body) - if "error" in reply: - raise RuntimeError(reply["error"]) - return reply["result"] - - def tool(self, name, **arguments): - result = self.request("tools/call", {"name": name, "arguments": arguments}) - if result.get("isError"): - raise RuntimeError(result) - text = next(item["text"] for item in result["content"] if item["type"] == "text") - try: - return json.loads(text) - except json.JSONDecodeError: - return text - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("output", type=Path) - parser.add_argument("--endpoint", default="http://127.0.0.1:2511/mcp") - parser.add_argument("--frames", type=int, default=0, help="Also capture this many walk frames at 20 fps.") - args = parser.parse_args() - args.output.mkdir(parents=True, exist_ok=True) - client = MCP(args.endpoint) - report = {"renderGraph": client.tool("render_graph.get"), "captures": []} - cameras = [camera for camera in report["renderGraph"]["cameras"] if camera["isActive"]] - if len(cameras) != 1: - raise RuntimeError("This demo expects exactly one active render camera.") - camera_id = cameras[0]["entityId"] - client.tool("runtime.pause", reason="Repeatable native Spine capture") - poses = [("walk", 0.3), ("run", 0.2), ("jump", 0.35)] - poses += [("walk", index / 20) for index in range(args.frames)] - try: - for index, (animation, time) in enumerate(poses): - client.tool("spine.pose", animation=animation, time=time, playing=False) - # Transform propagation and camera preparation run in successive phases. - client.tool("runtime.step_frame", frames=2) - capture = client.tool("render.capture_screenshot", cameraEntityId=camera_id) - if capture["width"] <= 1 or capture["height"] <= 1: - raise RuntimeError(f"Empty native capture: {capture}") - name = f"{animation}.png" if index < 3 else f"frame-{index - 3:03d}.png" - shutil.copy2(capture["path"], args.output / name) - report["captures"].append(dict(capture, animation=animation, time=time, savedAs=name)) - print(f"{name}: {capture['width']}x{capture['height']}", flush=True) - sleep(0.15) - finally: - (args.output / "capture-report.json").write_text(json.dumps(report, indent=2), encoding="utf-8") - client.tool("spine.pose", animation="walk", time=0, playing=True) - client.tool("runtime.resume") - - -if __name__ == "__main__": - main() diff --git a/Demos/SpinePreview/Assets/SpineRuntime-LICENSE.txt b/Demos/SpinePreview/Assets/SpineRuntime-LICENSE.txt deleted file mode 100644 index 517ff9fbf..000000000 --- a/Demos/SpinePreview/Assets/SpineRuntime-LICENSE.txt +++ /dev/null @@ -1,26 +0,0 @@ -Spine Runtimes License Agreement -Last updated April 5, 2025. Replaces all prior versions. - -Copyright (c) 2013-2025, Esoteric Software LLC - -Integration of the Spine Runtimes into software or otherwise creating -derivative works of the Spine Runtimes is permitted under the terms and -conditions of Section 2 of the Spine Editor License Agreement: -https://esotericsoftware.com/spine-editor-license - -Otherwise, it is permitted to integrate the Spine Runtimes into software -or otherwise create derivative works of the Spine Runtimes (collectively, -"Products"), provided that each user of the Products must obtain their own -Spine Editor license and redistribution of the Products in any form must -include this license and copyright notice. - -THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY -EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, -BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Demos/SpinePreview/Assets/UPSTREAM.md b/Demos/SpinePreview/Assets/UPSTREAM.md deleted file mode 100644 index 432fdd348..000000000 --- a/Demos/SpinePreview/Assets/UPSTREAM.md +++ /dev/null @@ -1,5 +0,0 @@ -Spineboy binary skeleton, atlas and image are unmodified official Spine 4.2 example assets. - -https://github.com/EsotericSoftware/spine-runtimes/tree/e7dc1435fa4a0083ab431f1b28e083c14a1f5c68/examples/spineboy/export - -Copyright Esoteric Software LLC. See SpineRuntime-LICENSE.txt. diff --git a/Demos/SpinePreview/Assets/preview.html b/Demos/SpinePreview/Assets/preview.html deleted file mode 100644 index ab2726d44..000000000 --- a/Demos/SpinePreview/Assets/preview.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - - -AdaEngine · Spine 2D - - - -
-
AdaEngine / Spine 4.2

Персонаж оживает

Ходьба, бег и прыжок — с настоящим движением костей и деформацией сетки.

Кадры из Swift runtime
-
-
Ходьба
Кости персонажа
-
-
0.00 / 2.00 s
-
-
Spineboy · официальный пример Esoteric Software
Рассчитано AdaSpineRuntime → Spine C 4.2 → вершины и кости
Браузерный WebGL-просмотр кадров
Не является снимком нативного GPU-рендера AdaEngine
-
- - - - diff --git a/Demos/SpinePreview/Assets/spineboy-pro.skel b/Demos/SpinePreview/Assets/spineboy-pro.skel deleted file mode 100644 index 09e564b7edc42fe37be66a4a0391862071ed948a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64463 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�<^fqXB8moaV!V_K@$T-Y7QvC8ml^- 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 diff --git a/Demos/SpinePreview/Assets/spineboy.atlas b/Demos/SpinePreview/Assets/spineboy.atlas deleted file mode 100644 index eca542b71..000000000 --- a/Demos/SpinePreview/Assets/spineboy.atlas +++ /dev/null @@ -1,94 +0,0 @@ -spineboy.png - size: 1024, 256 - filter: Linear, Linear - scale: 0.5 -crosshair - bounds: 352, 7, 45, 45 -eye-indifferent - bounds: 862, 105, 47, 45 -eye-surprised - bounds: 505, 79, 47, 45 -front-bracer - bounds: 826, 66, 29, 40 -front-fist-closed - bounds: 786, 65, 38, 41 -front-fist-open - bounds: 710, 51, 43, 44 - rotate: 90 -front-foot - bounds: 210, 6, 63, 35 -front-shin - bounds: 665, 128, 41, 92 - rotate: 90 -front-thigh - bounds: 2, 2, 23, 56 - rotate: 90 -front-upper-arm - bounds: 250, 205, 23, 49 -goggles - bounds: 665, 171, 131, 83 -gun - bounds: 798, 152, 105, 102 -head - bounds: 2, 27, 136, 149 -hoverboard-board - bounds: 2, 178, 246, 76 -hoverboard-thruster - bounds: 722, 96, 30, 32 - rotate: 90 -hoverglow-small - bounds: 275, 81, 137, 38 -mouth-grind - bounds: 614, 97, 47, 30 -mouth-oooo - bounds: 612, 65, 47, 30 -mouth-smile - bounds: 661, 64, 47, 30 -muzzle-glow - bounds: 382, 54, 25, 25 -muzzle-ring - bounds: 275, 54, 25, 105 - rotate: 90 -muzzle01 - bounds: 911, 95, 67, 40 - rotate: 90 -muzzle02 - bounds: 792, 108, 68, 42 -muzzle03 - bounds: 956, 171, 83, 53 - rotate: 90 -muzzle04 - bounds: 275, 7, 75, 45 -muzzle05 - bounds: 140, 3, 68, 38 -neck - bounds: 250, 182, 18, 21 -portal-bg - bounds: 140, 43, 133, 133 -portal-flare1 - bounds: 554, 65, 56, 30 -portal-flare2 - bounds: 759, 112, 57, 31 - rotate: 90 -portal-flare3 - bounds: 554, 97, 58, 30 -portal-shade - bounds: 275, 121, 133, 133 -portal-streaks1 - bounds: 410, 126, 126, 128 -portal-streaks2 - bounds: 538, 129, 125, 125 -rear-bracer - bounds: 857, 67, 28, 36 -rear-foot - bounds: 663, 96, 57, 30 -rear-shin - bounds: 414, 86, 38, 89 - rotate: 90 -rear-thigh - bounds: 756, 63, 28, 47 -rear-upper-arm - bounds: 60, 5, 20, 44 - rotate: 90 -torso - bounds: 905, 164, 49, 90 diff --git a/Demos/SpinePreview/Assets/spineboy.png b/Demos/SpinePreview/Assets/spineboy.png deleted file mode 100644 index 0ea9737f30707915e0bf495cdf987c6511dc0500..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 245321 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 z
  • sZ@kf@gL7X)y;k|NZYN7CI4}AlT7Z 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

    $ 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%0DxD_{VYG+f0?szS)DJk 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 kPx# diff --git a/Demos/SpinePreview/README.md b/Demos/SpinePreview/README.md deleted file mode 100644 index 10e1056b7..000000000 --- a/Demos/SpinePreview/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Spine runtime visual preview - -Export 240 real poses of the official Spineboy example with the new -`AdaSpineRuntime`, including skinned/clipped geometry and bone endpoints: - -```powershell -$env:ADAENGINE_HEADLESS = '1' -swift run SpinePreview C:/path/to/spine-preview -python -m http.server 8769 --bind 127.0.0.1 --directory C:/path/to/spine-preview -``` - -Open `http://127.0.0.1:8769`. Select walk, run or jump; show the skeleton, pause, -change playback speed or scrub individual frames. - -The Swift executable samples the production player. The browser only draws the -exported triangles and bone endpoints; it does not implement or imitate Spine -animation. This is a portable WebGL preview, **not proof of AdaEngine's native GPU -draw pass**. The production headless integration tests cover extraction and GPU -buffer preparation separately. See `Documentation/Spine2D.md` for engine usage. - -The example character, skeleton and atlas are official Esoteric Software assets -from the pinned upstream commit. Source and license notices are included in the -exported preview directory. No generated animation data is checked into the repo. diff --git a/Demos/SpinePreview/SpinePreview.swift b/Demos/SpinePreview/SpinePreview.swift deleted file mode 100644 index fee1d4d46..000000000 --- a/Demos/SpinePreview/SpinePreview.swift +++ /dev/null @@ -1,80 +0,0 @@ -import AdaSpineRuntime -import Foundation - -/// Exports actual runtime poses for a portable visual preview, without substituting animation in JavaScript. -@main -struct SpinePreview { - struct Draw: Encodable { - let page: Int - let blend: Int - let pma: Bool - let vertices: [[Float]] - let indices: [UInt32] - } - struct Frame: Encodable { - let draws: [Draw] - let bones: [[Float]] - } - struct Clip: Encodable { - let name: String - let fps: Int - let frames: [Frame] - } - struct Preview: Encodable { - let source = "AdaSpineRuntime / official Spine C 4.2" - let clips: [Clip] - } - - static func main() throws { - let arguments = CommandLine.arguments - guard arguments.count == 2 else { - print("Usage: swift run SpinePreview ") - return - } - let output = URL(fileURLWithPath: arguments[1], isDirectory: true) - try FileManager.default.createDirectory(at: output, withIntermediateDirectories: true) - guard let assets = Bundle.module.resourceURL?.appendingPathComponent("Assets") else { - throw SpineError.invalidData("Preview assets are missing.") - } - let skeleton = try Data(contentsOf: assets.appendingPathComponent("spineboy-pro.skel")) - let atlas = try String(contentsOf: assets.appendingPathComponent("spineboy.atlas"), encoding: .utf8) - let player = try SpineAnimationPlayer(skeleton: skeleton, atlas: atlas, format: .binary) - print("Animations: \(player.animationNames.joined(separator: ", "))") - let fps = 30 - var clips: [Clip] = [] - for name in ["idle", "walk", "run", "jump"] where player.animationNames.contains(name) { - player.resetToSetupPose() - try player.play(name, repeats: name != "jump", transitionDuration: 0) - var frames: [Frame] = [] - for index in 0..<60 { - if index > 0 { player.advance(by: 1 / Float(fps)) } - frames.append(Frame( - draws: player.snapshot().map { draw in - Draw(page: draw.pageIndex, blend: draw.blendMode.rawValue, pma: draw.premultipliedAlpha, - vertices: draw.vertices.map { vertex in - [round(vertex.x), round(vertex.y), round(vertex.u), round(vertex.v), - round(vertex.red), round(vertex.green), round(vertex.blue), round(vertex.alpha), - round(vertex.darkRed), round(vertex.darkGreen), round(vertex.darkBlue), round(vertex.darkAlpha)] - }, indices: draw.indices) - }, - bones: player.boneSnapshot().filter(\.isActive).map { [round($0.x), round($0.y), round($0.endX), round($0.endY)] } - )) - } - clips.append(Clip(name: name, fps: fps, frames: frames)) - } - let data = try JSONEncoder().encode(Preview(clips: clips)) - try data.write(to: output.appendingPathComponent("poses.json")) - var script = Data("window.SPINE_PREVIEW_DATA=".utf8) - script.append(data) - script.append(Data(";".utf8)) - try script.write(to: output.appendingPathComponent("poses.js")) - for name in ["spineboy.png", "preview.html", "SpineRuntime-LICENSE.txt", "UPSTREAM.md"] { - let content = try Data(contentsOf: assets.appendingPathComponent(name)) - let destination = name == "preview.html" ? "index.html" : name - try content.write(to: output.appendingPathComponent(destination)) - } - print("Exported \(clips.count) animations / \(clips.reduce(0) { $0 + $1.frames.count }) runtime frames to \(output.path)") - } - - private static func round(_ value: Float) -> Float { (value * 100_000).rounded() / 100_000 } -} diff --git a/Demos/Sponza/Camera/SponzaCameraMotion.swift b/Demos/Sponza/Camera/SponzaCameraMotion.swift deleted file mode 100644 index 348016a3c..000000000 --- a/Demos/Sponza/Camera/SponzaCameraMotion.swift +++ /dev/null @@ -1,30 +0,0 @@ -import AdaTransform -import Foundation - -/// Repeatable six-second gallery controls, evaluated from wall time rather than frame count. -public enum SponzaCameraMotion: String, CaseIterable, Sendable { - case stationary - case translation - case rotation - - public func pose(from origin: Transform, seconds: Double) -> Transform { - guard seconds.isFinite else { return origin } - let phase = seconds.remainder(dividingBy: 6) * .pi / 3 - var pose = origin - switch self { - case .stationary: - break - case .translation: - // The upper gallery runs along world X. Keep height, heading and corridor Z fixed. - pose.position.x += Float(1.5 * (1 - cos(phase))) - case .rotation: - var camera = SponzaFlyCamera() - camera.setPose(origin) - pose.rotation = SponzaFlyCamera.rotation( - yaw: camera.yaw + Float(phase), - pitch: camera.pitch + Float(0.2 * sin(2 * phase)) - ) - } - return pose - } -} diff --git a/Demos/Sponza/Camera/SponzaFlyCamera.swift b/Demos/Sponza/Camera/SponzaFlyCamera.swift deleted file mode 100644 index 2d225fb37..000000000 --- a/Demos/Sponza/Camera/SponzaFlyCamera.swift +++ /dev/null @@ -1,48 +0,0 @@ -import AdaTransform -import Foundation -import Math - -/// +Z forward, yaw about world Y, then pitch about the camera's local X. No roll. -public struct SponzaFlyCamera { - public private(set) var transform = Transform() - public private(set) var yaw: Float = 0 - public private(set) var pitch: Float = 0 - - public init() {} - - public mutating func setPose(_ pose: Transform) { - transform = pose - let forward = pose.matrix.z - yaw = Math.atan2(forward.x, forward.z) - pitch = -asin(max(-1, min(1, forward.y))) - } - - public static func rotation(yaw: Float, pitch: Float) -> Quat { - let horizontal = Quat(axis: [0, 1, 0], angle: yaw) - let vertical = Quat(axis: [1, 0, 0], angle: pitch) - // Hamilton product Y * X; spelling out the axes avoids legacy Euler conventions. - return Quat(x: horizontal.w * vertical.x, y: horizontal.y * vertical.w, - z: -horizontal.y * vertical.x, w: horizontal.w * vertical.w).normalized - } - - public static func looking(from position: Vector3, at target: Vector3) -> Transform { - let direction = (target - position).normalized - return Transform(rotation: rotation(yaw: Math.atan2(direction.x, direction.z), pitch: -asin(max(-1, min(1, direction.y)))), position: position) - } - - public mutating func look(delta: Vector2) { - guard delta.x.isFinite, delta.y.isFinite, delta != .zero else { return } - yaw = (yaw + delta.x * 0.003).remainder(dividingBy: 2 * .pi) - pitch = max(-1.55, min(1.55, pitch + delta.y * 0.003)) - transform.rotation = Self.rotation(yaw: yaw, pitch: pitch) - } - - public mutating func move(local: Vector3, seconds: Double, fast: Bool) { - guard seconds.isFinite, seconds > 0 else { return } - let basis = transform.matrix - var direction = Vector3(basis.x.x, basis.x.y, basis.x.z) * local.x - + Vector3(0, local.y, 0) + Vector3(basis.z.x, basis.z.y, basis.z.z) * local.z - if direction.length > 1 { direction = direction.normalized } - transform.position += direction * Float(min(seconds, 0.05)) * (fast ? 10 : 3) - } -} diff --git a/Demos/Sponza/Camera/SponzaLightMotion.swift b/Demos/Sponza/Camera/SponzaLightMotion.swift deleted file mode 100644 index 0c42b4add..000000000 --- a/Demos/Sponza/Camera/SponzaLightMotion.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation -import Math - -/// A repeatable twelve-second flight through the upper gallery. -public enum SponzaLightMotion { - public static func position(seconds: Double) -> Vector3 { - let phase: Double = (seconds.isFinite ? seconds : 0).remainder(dividingBy: 12) * .pi / 6 - let x = Float(8 * Foundation.sin(phase)) - let y = Float(7.4 + 0.6 * Foundation.sin(2 * phase)) - let z = Float(5.35 + 0.5 * Foundation.cos(phase)) - return Vector3(x, y, z) - } -} diff --git a/Demos/Sponza/Package.resolved b/Demos/Sponza/Package.resolved deleted file mode 100644 index eab0eb973..000000000 --- a/Demos/Sponza/Package.resolved +++ /dev/null @@ -1,140 +0,0 @@ -{ - "originHash" : "995820e9e58c7f70d39b8638eda0773f78b4a426aff725a5bcf3802f46790aab", - "pins" : [ - { - "identity" : "swan", - "kind" : "remoteSourceControl", - "location" : "https://github.com/AdaEngine/swan", - "state" : { - "revision" : "790df787281e9cf1c2358a11bb42b2ec2df01029" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", - "state" : { - "revision" : "6a52f3251125d74daf04fcbd5e6f08a75d074382", - "version" : "1.8.2" - } - }, - { - "identity" : "swift-atomics", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-atomics", - "state" : { - "revision" : "0442cb5a3f98ab802acb777929fdb446bda11a34", - "version" : "1.3.1" - } - }, - { - "identity" : "swift-cmark", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-cmark.git", - "state" : { - "revision" : "08ddb528923cc1a6527e02b7a1aee9e516ca749a", - "version" : "0.9.0" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "f96b909e1a9e469620bc5037c1e24e00153f376f", - "version" : "1.7.5" - } - }, - { - "identity" : "swift-distributed-tracing", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-distributed-tracing", - "state" : { - "revision" : "789663680a3a4767e698be46bc154a3bc5c64fef", - "version" : "1.7.3" - } - }, - { - "identity" : "swift-format", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-format.git", - "state" : { - "revision" : "d54c5be7afba3e5f52ae29e2371e444a3c2a49c1", - "version" : "603.0.0" - } - }, - { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log", - "state" : { - "revision" : "c9c6bb9d275ec0d95c3ad16391f3f045dc623104", - "version" : "1.16.2" - } - }, - { - "identity" : "swift-markdown", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-markdown.git", - "state" : { - "revision" : "25cb61d3482054b09ae76ca4f281b1bfe7fe5a43", - "version" : "0.9.0" - } - }, - { - "identity" : "swift-service-context", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-service-context", - "state" : { - "revision" : "853ba3d9ce78851b9cc20108e8bd4f13f7fa546c", - "version" : "1.5.1" - } - }, - { - "identity" : "swift-subprocess", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-subprocess.git", - "state" : { - "branch" : "0.2.1", - "revision" : "44922dfe46380cd354ca4b0208e717a3e92b13dd" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-syntax.git", - "state" : { - "revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1", - "version" : "603.0.2" - } - }, - { - "identity" : "swift-system", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-system", - "state" : { - "revision" : "869129b7bf4ecc57b97d0193ad29690ca2134750", - "version" : "1.8.1" - } - }, - { - "identity" : "yams", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SpectralDragon/Yams.git", - "state" : { - "branch" : "fb676da", - "revision" : "fb676da5b91f677ab8e8d06b7fcde45b25181531" - } - }, - { - "identity" : "zlib", - "kind" : "remoteSourceControl", - "location" : "https://github.com/the-swift-collective/zlib.git", - "state" : { - "revision" : "f1d153b90420f9fcc6ef916cd67ea96f0e68d137", - "version" : "1.3.2" - } - } - ], - "version" : 3 -} \ No newline at end of file diff --git a/Demos/Sponza/Package.swift b/Demos/Sponza/Package.swift deleted file mode 100644 index 1d599754f..000000000 --- a/Demos/Sponza/Package.swift +++ /dev/null @@ -1,19 +0,0 @@ -// swift-tools-version: 6.2 -import PackageDescription - -let package = Package( - name: "SponzaDemo", - platforms: [.macOS(.v15)], - dependencies: [.package(name: "AdaEngine", path: "../..")], - targets: [ - .executableTarget( - name: "SponzaDemo", - dependencies: ["SponzaCamera", .product(name: "AdaEngine", package: "AdaEngine"), .product(name: "Math", package: "AdaEngine")], - path: ".", - exclude: ["README.md", "Tests", "Camera"], - swiftSettings: [.enableUpcomingFeature("MemberImportVisibility"), .strictMemorySafety()] - ), - .target(name: "SponzaCamera", dependencies: [.product(name: "AdaTransform", package: "AdaEngine"), .product(name: "Math", package: "AdaEngine")], path: "Camera"), - .testTarget(name: "SponzaDemoTests", dependencies: ["SponzaCamera", .product(name: "AdaTransform", package: "AdaEngine")], path: "Tests") - ] -) diff --git a/Demos/Sponza/README.md b/Demos/Sponza/README.md deleted file mode 100644 index 7aed4eb57..000000000 --- a/Demos/Sponza/README.md +++ /dev/null @@ -1,178 +0,0 @@ -# Sponza windowed demo - -Fetch the external dataset first with `python script/fetch_benchmark_scene.py` from the engine root. -The original assets and licenses stay in `.build-codex/benchmark-assets/intel-sponza`. - -Run `swift run --package-path Demos/Sponza -c release SponzaDemo`. -`ADAENGINE_SPONZA_ASSETS` can override the prepared assets directory. -On Windows, use the same Swift/Dawn/Tint configuration as the native renderer benchmark. - -WASD moves, Q/E changes world height, hold the right mouse button to look, and Shift increases speed. -Escape releases mouse capture until the right button is released. Yaw uses world Y and pitch uses local X; -there is no roll. The toolbar selects atrium, corridor, gallery or outside and toggles Hi-Z. -There is no collision controller. PBR textures, three shadow cascades and +3 EV match the benchmark profile. - -Set `ADAENGINE_SPONZA_PROFILE_OUTPUT` to an absolute JSON filename to measure actual SceneView publication -intervals without image readback. After loading, the profiler visits atrium, gallery and outside with three -alternating pairs of shadow-on/off measurements (one second warmup, two seconds measured per case). -Keep window size constant during the entire run; discard a case if its size changes. Camera input is disabled -while profiling. The profiler restores shadows and the outside viewpoint when it finishes. -Graph records contain CPU encoding times, not GPU timestamp measurements. Use the opt-in rendering tests -for the reproducible meshlet benchmark, and the window profile to investigate interactive frame pacing. - -`ADAENGINE_WEBGPU_LEGACY_COMPLETION=1` selects the old native callback polling path for diagnostic comparisons. -Leave it unset during normal use. - -For moving-camera measurements, also set `ADAENGINE_SPONZA_PROFILE_MOTION=1`. -This compares the stationary gallery pose, a 0–3 metre translation along gallery X, and a full 360-degree -turn with a small pitch sweep. Motion follows a six-second wall-clock cycle through the same ECS camera -component write path as interactive movement. Each case has one second warmup and six seconds measured; -three alternating shadow-on/off pairs are recorded for each motion. `motion` and `cameraUpdates` identify -what actually moved. Use `stationary`, `translation` or `rotation` instead of `1` to run only that motion. -The scripted motion does not emulate native mouse events or an arbitrary user flight path. - -## Lighting and path tracing - -The window now has three explicit presets: full 8.81M-triangle raster geometry, raster lighting LOD, -and path tracing on the same lighting LOD. The default interactive preset is full-scene raster with the 12 local lights. -`ADAENGINE_SPONZA_RENDER_PRESET=fullRaster|lodRaster|pathTracing` selects startup mode. -The light button cycles 0 / 4 / 12 local lights; the sun stays enabled. -Four warm gallery spots cast shadows; eight warm/cool point fill lights do not. -The demo now defaults to 2048 pixels per sun cascade and 1024 per local-light face/tile. -Sun-shadow coverage also increases from 30 to 80 metres with closer cascade splits (lambda 0.85). -The shadow button switches back to the earlier 1024/512, 30-metre setting. Other scenes retain their defaults. -Raster local-shadow budget remains four lights; the renderer selects nearby shadow-casting sources, -including the moving test light, within that budget. PT shadow rays do not use this raster atlas budget. -The separate flying-light button enables a shadow-casting point light on a twelve-second gallery orbit. -It starts enabled for interactive testing, independently of the 0 / 4 / 12 stationary lights. -Turn it off to inspect static PT accumulation; moving light intentionally resets that accumulation. - -The portable shader-BVH tracer supports at most 131072 triangles. Prepare the distinct lighting LOD first: -`python script/prepare_sponza_path_tracing.py --gltfpack `. -The current preparation yields 122286 source triangles, preserving image references and keeping the originals. -The tool records output hashes and its executable hash in `assets/lighting-pt-receipt.json`. -Pinned tool source: https://github.com/zeux/meshoptimizer/releases/tag/v1.3 ; Windows archive SHA-256: -`f6e9c09d66af23da3da71b86f0652d2413e81c734e0aecd1cd3d0e6e6e8645c0`. -This is a strongly simplified geometry preset; UVs and normals participate in attribute-aware simplification. -The relative geometric error is limited to 1%, with a larger triangle allocation for architecture than ivy. -Preparation rejects a result that loses an entire source material group. The former 100% error profile -discarded the floor and wall groups and must be regenerated. This LOD does not reproduce full-scene geometric detail. - -PT uses 1 sample per pixel, 2 interactions and up to 128 accumulated static frames. -Without DLSS the quality button selects 1/2 width and height or the faster 1/4 preview. -With DLSS it selects Quality (2/3 input scale) or Performance (1/2); PT traces at that input size, -without a second internal resolution reduction. DLSS Super Resolution is enabled by default when available. -Its separate status line reports actual encoded frames and fallback frames. Unsupported devices retain -the ordinary rendering path and report that DLSS is unavailable. -Camera/light changes reset accumulation. It is software BVH traversal, not hardware RTX. -The demo uses 512-pixel PT material tiles (the engine default remains 128); raster still samples the -original textures. The comparison is a renderer workload comparison, not an equal-image-quality benchmark. -The status line reports actual encoded geometry, lights, ray-image size or a tracing failure. - -For the lighting benchmark set `ADAENGINE_SPONZA_PROFILE_LIGHTING=1` along with the profile output, -`ADAENGINE_SPONZA_RENDER_PRESET=lodRaster`, and optionally `ADAENGINE_SPONZA_PROFILE_MOTION=rotation`. -It measures the LOD in raster and path tracing with 0, 4 and 12 local lights, three rounds of six seconds -per case after one second warmup. Cold BVH/atlas construction is outside measurement and has a 180-second -readiness deadline. PT cases require actual encoded-frame progress, expected light count and no failure; -a fallback image does not count as a path-tracing result. Profiling disables DLSS and the flying light -to preserve the explicit stationary-light cases. Full-scene legacy profiles retain 0 local lights. - -For Windows DLSS builds, prepare and stage the matching native NGX runtime using -[Documentation/DLSS.md](../../Documentation/DLSS.md). This demo uses Super Resolution, not Frame Generation -or Ray Reconstruction. DLSS cannot recover geometry removed during asset preparation. - -Native camera history is retained while offscreen render targets are being prepared; removal of the actual -camera releases that history. GPU regression coverage exercises this lifecycle. During movement, this -experimental tracer resets accumulation; it has no temporal reprojection or production denoiser. -The current static PT image still has substantial visual defects even after accumulation and is not a -production-quality reference. See the accepted workload measurements and remaining limits in -[the lighting report](../../Documentation/Performance/SponzaLightingPerformance-2026-11-03.md). - -## Meshlets and cluster map - -Prepare clusters without simplifying the source scene: - -```text -python script/build_meshlet_tool.py -python script/prepare_meshlets.py /main_sponza/benchmark-pbr.gltf --builder native -python script/prepare_meshlets.py /pkg_b_ivy/benchmark-pbr.gltf --builder native -``` - -Keep each `.gltf.meshlets` file beside its source. Repeat preparation for `lighting-pt.gltf` to use -the smaller raster LOD. Missing or invalid sidecars retain ordinary geometry. The builder pins -meshoptimizer 1.3; no offline C++ dependency enters the runtime. See [Tools/Meshlets](../../Tools/Meshlets/README.md). - -The meshlet button cycles **disabled → cpuReference → gpu**. The engine default stays disabled; -this diagnostic demo explicitly selects GPU mode. `ADAENGINE_SPONZA_MESHLETS` overrides the startup mode. -GPU v1 conservatively culls prepared static opaque singleton draws by frustum and compacts their indices, -issuing one indirect draw per original coarse draw. Existing instancing, MASK/BLEND, deformed/graph/parallax -surfaces, force-visible meshes, unknown draw passes and unsupported devices retain the original path. -Custom passes must explicitly adopt `MeshletGeometryDrawPass` to promise static vertex coverage and consume -the main-pass geometry override. Hi-Z uses the existing -legacy path; combined meshlet Hi-Z/cone culling is not enabled in this version. -Fully contained parts bypass classification/index copying and use their complete original draw; `целиком` -reports this fast path separately from unsupported fallbacks. Partial parts use workgroup-parallel prefix -scan and three batched compute passes per view, preserving source primitive order. - -The separate **Карта мешлетов** button displays stable colors from actual local cluster IDs. Gray means -ordinary fallback geometry, not a missing surface. The map selects full-scene raster, disables DLSS for -sharp diagnostic colors and preserves the source silhouette. Its cached diagnostic vertex streams are -bounded to 768 MiB and created only when requested. Exact partition display uses one draw per eligible part; -adaptive LOD display coalesces the selected node ranges. Neither path issues one draw per cluster. -this diagnostic allocation/draw path is excluded from performance measurements and bypasses unused GPU -visibility compaction. Unsupported or over-budget -parts remain gray. Set `ADAENGINE_SPONZA_MESHLET_MAP=1` to start in this view. - -These meshlets retain the full source triangles and are separate from the simplified PT lighting LOD. -The raster visibility processor does not accelerate the software path tracer's BVH. - -`ADAENGINE_SPONZA_PROFILE_MESHLETS=1` with an absolute `ADAENGINE_SPONZA_PROFILE_OUTPUT` runs -disabled/CPU/GPU comparisons in gallery rotation and a stationary outside view: three alternating rounds, -one second warmup and four seconds measured per case. It fixes full geometry, disables DLSS, local/flying -lights, Hi-Z and the map, and leaves sun shadows enabled. The run first warms the outside overview for -three seconds and a complete gallery turn for six seconds. CPU-known cluster counts, fully visible and -fallback triangles are separate from GPU-submitted index counts, which are read back only after each -measured interval. SceneView publication -and CPU graph times are not GPU timestamps. The run ends in the full-scene meshlet map. - -The current Windows/RTX 4070 gallery benchmark does **not** meet the acceleration gate: about 103 FPS -in GPU mode versus 123 FPS in disabled mode at 1890×843 without local lights or DLSS. Keep the mode -explicit while investigating this cost. Correctness, raw measurements and remaining platform limits: -[implementation report](../../Documentation/Performance/MeshletsImplementation-2026-11-03.md). - -## Adaptive meshlet LOD - -The original visibility-only meshlets do not simplify geometry. Prepare the additional hierarchy to let -groups of clusters use coarser parents with distance: - -```text -python script/build_meshlet_tool.py -python script/prepare_meshlet_lod.py /main_sponza/benchmark-pbr.gltf -python script/prepare_meshlet_lod.py /pkg_b_ivy/benchmark-pbr.gltf -``` - -Keep `.gltf.meshletlod` beside the source model; `.gltf.meshlets` remains the exact v1 partition. -The builder preserves source vertex identities and exact group boundary edges; it does not cross materials. -Missing/stale/corrupt hierarchy retains the original geometry or v1 culling. Unreducible groups remain exact -forest roots instead of inventing parent levels. The prepared full architecture has L0–L4, Ivy L0–L1. - -The **LOD** button cycles Off / 1 / 2 / 4 / 8 output pixels of allowed approximation error; the demo starts at 2. -`ADAENGINE_SPONZA_MESHLET_LOD=0|1|2|4|8` sets startup quality. Engine settings default to zero, and the -meshlet `disabled` mode still uses original geometry. Different source regions choose levels independently. -In the map, actual selected parents replace their children: L1 is tinted cyan, L2 orange, L3 pink, L4 purple; -L0 retains the original cluster-ID colors. The status reports selected levels/triangles for the diagnostic -cut. Ordinary GPU mode reports active LOD items; it does not synchronously read GPU triangle counts. - -At the coarsest complete forest, opaque architecture changes from 3,738,878 to 2,172,290 triangles, -Ivy from 5,065,558 to 4,455,592. These are offline lower bounds, not expected counts at every camera position. -Shadow/contact passes retain full source geometry. Temporal motion guides use independent immutable indices -and the same LOD cut, including previous object transforms; they do not borrow a completed GPU frame's buffer. - -`ADAENGINE_SPONZA_PROFILE_MESHLET_LOD=1` compares GPU LOD Off / 2 / 8 with the same two camera paths and -three alternating rounds. It fixes the new 2048/1024 shadow quality with 80-metre sun coverage, disables DLSS, local/flying lights, -Hi-Z and the map during timing, and records the explicit LOD/shadow settings. Profiling fixes the SceneView -to 1120×400 logical points so maximizing the native window cannot change the measured resolution. -Historical profiles keep -LOD off and 1024/512 shadows. `ADAENGINE_SPONZA_SHADOW_QUALITY=baseline` selects old startup shadows. - -[LOD, shadows and final validation](../../Documentation/Performance/MeshletLODAndShadows-2026-11-03.md). diff --git a/Demos/Sponza/SponzaDemo.swift b/Demos/Sponza/SponzaDemo.swift deleted file mode 100644 index 28386928a..000000000 --- a/Demos/Sponza/SponzaDemo.swift +++ /dev/null @@ -1,370 +0,0 @@ -import AdaEngine -import Foundation -import Math -import Observation -import SponzaCamera - -struct CameraRoutes: Decodable { - struct Pose: Decodable { - var position: [Float] - var rotation: [Float] - - var transform: Transform { - Transform(rotation: Quat(x: rotation[0], y: rotation[1], z: rotation[2], w: rotation[3]), position: [position[0], position[1], position[2]]) - } - } - struct Route: Decodable { var id: String; var start: Pose } - var routes: [Route] -} - -@MainActor -@Observable -final class SponzaSession: Resource { - var status = "Загружаем Sponza + Ivy…" - var hiZ = false - var requestedView: Int? - var camera: Entity? - var poses: [CameraRoutes.Pose] = [] - var preset = SponzaRenderPreset.fullRaster - var lightCount = 12 - var requestedPreset: SponzaRenderPreset? - var requestedLightCount: Int? - var pathHighQuality = true - var requestedQuality: Bool? - var dlssAvailable = false - var dlssEnabled = true - var requestedDLSS: Bool? - var dlssStatus = "Проверяем DLSS…" - var flyingLightEnabled = true - var meshletMode = MeshletVisibilityMode3D.gpu - var meshletMapEnabled = false - var requestedMeshletMode: MeshletVisibilityMode3D? - var requestedMeshletMap: Bool? - var meshletLODError: Float = 2 - var requestedMeshletLODError: Float? - var highResolutionShadows = true - var requestedShadowQuality: Bool? - var meshletStatus = "Загружаем подготовленные мешлеты…" - @ObservationIgnored var meshletStatistics: Render3DMeshletStatistics? - @ObservationIgnored var flightSeconds = 0.0 - @ObservationIgnored var temporalStatistics: Render3DTemporalStatistics? - var rendererStatus = "Готовим освещение…" - @ObservationIgnored var fullGeometry: Entity? - @ObservationIgnored var lodGeometry: Entity? - @ObservationIgnored var sun: Entity? - @ObservationIgnored var lighting = SponzaLighting() - @ObservationIgnored var pathStatistics: Render3DPathTracingStatistics? - @ObservationIgnored var nextStatusUpdate = ContinuousClock.now - var isLoaded = false - @ObservationIgnored let warmupFrames = SponzaWarmupFrames() - @ObservationIgnored var flight = SponzaFlyCamera() - @ObservationIgnored var cancelledLook = false - @ObservationIgnored var profiler: SponzaDemoProfiler? - var loadTask: Task? - @ObservationIgnored weak var runtime: AppWorlds? - - func move(to index: Int) { - guard poses.indices.contains(index), let camera else { return } - let transform = poses[index].transform - camera.components[Transform.self] = transform - camera.components[GlobalTransform.self] = GlobalTransform(matrix: transform.matrix) - flight.setPose(transform) - } -} - -@main -struct SponzaDemo: App { - var body: some AppScene { - WindowGroup { SponzaView() } - .window(with: UIWindow.Configuration(title: "Sponza + Ivy · AdaEngine", frame: Rect(x: 0, y: 0, - width: ProcessInfo.processInfo.environment["ADAENGINE_SPONZA_PROFILE_STUTTER"] == "1" ? 2560 : 1280, - height: ProcessInfo.processInfo.environment["ADAENGINE_SPONZA_PROFILE_STUTTER"] == "1" ? 1400 : 800))) - } -} - -private struct SponzaView: View { - @State private var session = SponzaSession() - - var body: some View { - VStack(spacing: 8) { - HStack(spacing: 16) { - Button("Атриум") { session.requestedView = 0 } - Button("Коридор") { session.requestedView = 1 } - Button("Галерея") { session.requestedView = 2 } - Button("Снаружи") { session.requestedView = 3 } - Button(session.hiZ ? "Hi-Z: вкл." : "Hi-Z: выкл.") { session.hiZ.toggle() } - } - .disabled(!session.isLoaded) - Text(session.status) - HStack(spacing: 16) { - Button("Полная сцена · растр") { session.requestedPreset = .fullRaster } - Button("PT LOD · растр") { session.requestedPreset = .lodRaster } - Button("PT LOD · path tracing") { session.requestedPreset = .pathTracing } - Button("Локальный свет: \(session.lightCount)") { session.requestedLightCount = session.lightCount == 0 ? 4 : session.lightCount == 4 ? 12 : 0 } - Button(session.meshletMapEnabled ? "Карта: 1:1" : session.dlssEnabled && session.dlssAvailable - ? (session.pathHighQuality ? "DLSS: Quality" : "DLSS: Performance") - : (session.pathHighQuality ? "PT: качество ½" : "PT: предпросмотр ¼")) { session.requestedQuality = !session.pathHighQuality } - } - .disabled(!session.isLoaded) - HStack(spacing: 16) { - Button(session.dlssEnabled && session.dlssAvailable && !session.meshletMapEnabled ? "DLSS: вкл." : "DLSS: выкл.") { session.requestedDLSS = !session.dlssEnabled } - Button(session.flyingLightEnabled ? "Летающий свет: вкл." : "Летающий свет: выкл.") { session.flyingLightEnabled.toggle() } - Button(session.highResolutionShadows ? "Тени: высокие · 80 м" : "Тени: базовые · 30 м") { session.requestedShadowQuality = !session.highResolutionShadows } - SponzaDLSSStatus(session: session) - } - .disabled(!session.isLoaded) - SponzaRendererStatus(session: session) - HStack(spacing: 16) { - Button("Мешлеты: \(session.meshletMode.rawValue)") { - session.requestedMeshletMode = session.meshletMode == .disabled ? .cpuReference : session.meshletMode == .cpuReference ? .gpu : .disabled - } - Button(session.meshletMapEnabled ? "Карта мешлетов: вкл." : "Карта мешлетов: выкл.") { session.requestedMeshletMap = !session.meshletMapEnabled } - Button(session.meshletLODError > 0 ? "LOD: \(Int(session.meshletLODError)) px" : "LOD: выкл.") { - session.requestedMeshletLODError = session.meshletLODError == 0 ? 1 : session.meshletLODError >= 8 ? 0 : session.meshletLODError * 2 - } - } - .disabled(!session.isLoaded) - SponzaMeshletStatus(session: session) - if session.meshletMapEnabled && session.meshletLODError > 0 { - Text("L0 — исходные кластеры · L1 — голубой · L2 — оранжевый · L3 — розовый · L4 — фиолетовый") - } - Text("WASD — движение · Q/E — вниз/вверх · правая кнопка мыши — осмотр · Shift — быстрее") - SceneView(useSceneCameras: true, make: { app in - app.addPlugin(TransformPlugin()).addPlugin(InputPlugin()).addPlugin(RenderWorldPlugin()) - .addPlugin(CameraPlugin()).addPlugin(AssetsPlugin()).addPlugin(VisibilityPlugin()) - .addPlugin(ScenePlugin()).addPlugin(Core3DPlugin(pathTracingPresentation: true)).addPlugin(UpscalePlugin()).addPlugin(SponzaPlugin()) - app.insertResource(session) - session.runtime = app - }, updateContent: { _, _ in }, onFrameRendered: { texture in - session.warmupFrames.didPublish() - session.profiler?.didPresent(width: texture.width, height: texture.height) - }) - .frameCaptureLabel("SponzaDemo") - // Keep measured GPU work independent of native-window resizing/maximization. - .frame(width: session.profiler?.isRunning == true && session.profiler?.usesFixedViewport == true ? 1120 : nil, - height: session.profiler?.isRunning == true && session.profiler?.usesFixedViewport == true ? 400 : nil) - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - .padding(10) - .background(Color(0.06, 0.07, 0.09, 1)) - .foregroundColor(.white) - } -} - -private struct SponzaPlugin: Plugin { - func setup(in app: AppWorlds) { - app.addSystem(SponzaStartupSystem.self, on: .startup) - app.addSystem(SponzaControlsSystem.self, on: .preUpdate) - } -} - -@System -@MainActor -func SponzaStartup(_ context: WorldUpdateContext, _ resource: Res) { - let session = resource.wrappedValue - guard let app = session.runtime else { return } - // Keep the window rendering and accepting input while the asset actor decodes the 4K textures. - // The session owns this task for the lifetime of this single-window demo; it is not detached. - session.loadTask = Task { @MainActor in - do { - let repository = ProcessInfo.processInfo.environment["ADAENGINE_SHADER_SOURCE_ROOT"].map { URL(fileURLWithPath: $0) } - ?? URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent() - let root = ProcessInfo.processInfo.environment["ADAENGINE_SPONZA_ASSETS"].map { URL(fileURLWithPath: $0, isDirectory: true) } - ?? repository.appendingPathComponent(".build-codex/benchmark-assets/intel-sponza/assets") - let routes = try JSONDecoder().decode(CameraRoutes.self, from: Data(contentsOf: repository.appendingPathComponent("Tools/3DAssets/BenchmarkScenes/intel-sponza-routes.json"))) - guard routes.routes.count == 3, routes.routes.allSatisfy({ $0.start.position.count == 3 && $0.start.rotation.count == 4 }) else { - throw CocoaError(.fileReadCorruptFile) - } - session.poses = routes.routes.map(\.start) - let outside = SponzaFlyCamera.looking(from: [-32, 25, -35], at: [0, 4, 0]) - session.poses.append(CameraRoutes.Pose(position: [outside.position.x, outside.position.y, outside.position.z], - rotation: [outside.rotation.x, outside.rotation.y, outside.rotation.z, outside.rotation.w])) - var environment = Environment3D(antiAliasing: .none) - environment.skybox.isEnabled = false - environment.shadows.isEnabled = true - environment.shadowCascadeCount = 3 - environment.shadows.resolution = 2048 - environment.ambientOcclusion.isEnabled = false - environment.screenSpaceReflection.isEnabled = false - environment.exposureEV = 3 - environment.gpuOcclusionEnabled = session.hiZ - let environmentRoot = try await app.withWorldAccess { - session.camera = app.main.spawn("Camera", bundle: Camera3D( - camera: Camera(), perspectiveProjection: PerspectiveProjection(near: 0.05, far: 200, fieldOfView: .degrees(60)), - transform: routes.routes[0].start.transform, environment: environment - )) - session.move(to: 0) - session.camera?.components[MouseCaptureComponent.self] = MouseCaptureComponent() - let lighting = app.main.spawn("Lighting") { Transform() } - let sun = app.main.spawn("Sun") { - DirectionalLightComponent(intensity: 3, castShadows: true) - Transform(rotation: Quat.euler([-0.4, 0.5, 0])) - } - lighting.addChild(sun) - session.sun = sun - session.lighting.spawn(in: app.main, parent: lighting) - let root = app.main.spawn("Environment") { Transform() } - session.fullGeometry = app.main.spawn("Full Sponza + Ivy") { Transform(); Visibility.hidden } - session.lodGeometry = app.main.spawn("PT lighting LOD") { Transform(); Visibility.hidden } - if let full = session.fullGeometry { root.addChild(full) } - if let lod = session.lodGeometry { root.addChild(lod) } - return root - } - _ = environmentRoot - for (filename, isLOD) in [("benchmark-pbr.gltf", false), ("lighting-pt.gltf", true)] { - for (folder, label) in [("main_sponza", "Sponza"), ("pkg_b_ivy", "Ivy")] { - try Task.checkCancellation() - session.status = "Загружаем \(label)\(isLOD ? " · PT LOD" : "")…" - let file = root.appendingPathComponent(folder).appendingPathComponent(filename) - guard let model = try await AssetsManager.load(ModelAsset3D.self, at: file.path).asset else { throw CocoaError(.fileReadCorruptFile) } - try await app.withWorldAccess { - let parent = isLOD ? session.lodGeometry : session.fullGeometry - parent?.addChild(model.instantiate(in: app.main)) - } - } - } - try await app.withWorldAccess { - session.pathStatistics = app.getSubworldBuilder(by: .renderWorld)?.getResource(Render3DPathTracingStatistics.self) - session.temporalStatistics = app.getSubworldBuilder(by: .renderWorld)?.getResource(Render3DTemporalStatistics.self) - session.meshletStatistics = app.getSubworldBuilder(by: .renderWorld)?.getResource(Render3DMeshletStatistics.self) - session.meshletMode = ProcessInfo.processInfo.environment["ADAENGINE_SPONZA_MESHLETS"].flatMap(MeshletVisibilityMode3D.init(rawValue:)) ?? .gpu - session.meshletMapEnabled = ProcessInfo.processInfo.environment["ADAENGINE_SPONZA_MESHLET_MAP"] == "1" - if let value = ProcessInfo.processInfo.environment["ADAENGINE_SPONZA_MESHLET_LOD"].flatMap(Float.init), value.isFinite { - session.meshletLODError = min(max(value, 0), 8) - } - session.highResolutionShadows = ProcessInfo.processInfo.environment["ADAENGINE_SPONZA_SHADOW_QUALITY"] != "baseline" - session.dlssAvailable = app.getSubworldBuilder(by: .renderWorld)?.getResource(RenderDeviceHandler.self)?.renderDevice.supportsTemporalUpscaling ?? false - let requested = ProcessInfo.processInfo.environment["ADAENGINE_SPONZA_RENDER_PRESET"].flatMap(SponzaRenderPreset.init(rawValue:)) ?? .fullRaster - session.apply(preset: requested, lights: 12) - session.move(to: 2) - } - try await session.warmupRaster(app: app) - session.isLoaded = true - session.status = "Sponza + Ivy · полная сцена / отдельный PT LOD" - FileHandle.standardOutput.write(Data("SPONZA_DEMO_READY\n".utf8)) - if let output = ProcessInfo.processInfo.environment["ADAENGINE_SPONZA_PROFILE_OUTPUT"] { - let profiler = SponzaDemoProfiler(output: URL(fileURLWithPath: output)) - session.profiler = profiler - try await profiler.run(session: session, app: app) - } - } catch { - session.status = session.isLoaded ? "Замер остановлен; подробности в журнале" : "Не удалось завершить загрузку: \(error)" - FileHandle.standardError.write(Data("SPONZA_DEMO_ERROR \(error)\n".utf8)) - } - } -} - -private struct SponzaRendererStatus: View { - let session: SponzaSession - - var body: some View { Text(session.rendererStatus) } -} - -private struct SponzaDLSSStatus: View { - let session: SponzaSession - var body: some View { Text(session.dlssStatus) } -} - -private struct SponzaMeshletStatus: View { - let session: SponzaSession - var body: some View { Text(session.meshletStatus) } -} - -@PlainSystem(dependencies: [.after(InputEventParseSystem.self), .before(CameraSystem.self)]) -struct SponzaControlsSystem { - @Res private var session - @Res private var input - @Res private var time - // Declare writes performed through the session's camera ID. - @Query, Ref, Ref, Ref, Ref> private var cameraAccess - @Query, Ref, PointLightComponent> private var lightAccess - @Query> private var visibilityAccess - @Query> private var sunAccess - - init(world _: World) {} - - @MainActor - func update(context _: UpdateContext) { - _ = cameraAccess - _ = visibilityAccess - _ = lightAccess - _ = sunAccess - guard session.isLoaded, let camera = session.camera, - let transform = camera.components[Transform.self] else { return } - session.refreshPathStatus() - if let profiler = session.profiler, profiler.isRunning { - if let next = profiler.cameraPose(), next != transform { - camera.components[Transform.self] = next - camera.components[GlobalTransform.self] = GlobalTransform(matrix: next.matrix) - profiler.didMoveCamera() - } - return - } - if session.isLoaded, session.requestedPreset != nil || session.requestedLightCount != nil || session.requestedQuality != nil - || session.requestedDLSS != nil || session.requestedMeshletMode != nil || session.requestedMeshletMap != nil - || session.requestedMeshletLODError != nil || session.requestedShadowQuality != nil { - if let quality = session.requestedQuality { session.pathHighQuality = quality } - if let dlss = session.requestedDLSS { session.dlssEnabled = dlss } - if let quality = session.requestedShadowQuality { session.highResolutionShadows = quality } - if let error = session.requestedMeshletLODError { - session.meshletLODError = error - session.hiZ = false - if error > 0 && session.meshletMode == .disabled { session.meshletMode = .gpu } - if session.preset == .pathTracing { session.requestedPreset = .fullRaster } - } - if session.requestedPreset == .pathTracing { session.meshletMapEnabled = false } - if let mode = session.requestedMeshletMode { - session.meshletMode = mode - session.hiZ = false - if session.preset == .pathTracing { session.requestedPreset = .fullRaster } - } - if let map = session.requestedMeshletMap { - session.meshletMapEnabled = map - if map { session.hiZ = false; session.requestedPreset = .fullRaster } - } - session.apply(preset: session.requestedPreset ?? session.preset, lights: session.requestedLightCount ?? session.lightCount) - session.requestedPreset = nil - session.requestedLightCount = nil - session.requestedQuality = nil - session.requestedDLSS = nil - session.requestedMeshletMode = nil - session.requestedMeshletMap = nil - session.requestedMeshletLODError = nil - session.requestedShadowQuality = nil - } - session.flightSeconds += max(0, Double(time.deltaTime)) - session.lighting.updateFlight(enabled: session.flyingLightEnabled, seconds: session.flightSeconds) - if let index = session.requestedView { - session.move(to: index) - session.requestedView = nil - } - let rightDown = input.isMouseButtonPressed(.right) - if !rightDown { session.cancelledLook = false } - if input.isKeyPressed(.escape) { session.cancelledLook = true } - if var capture = camera.components[MouseCaptureComponent.self] { - let requested = rightDown && !session.cancelledLook - if requested && capture.isCaptured { session.flight.look(delta: input.getMouseDelta()) } - if capture.isRequested != requested { - capture.isRequested = requested - camera.components[MouseCaptureComponent.self] = capture - } - } - var movement = Vector3.zero - if input.isKeyPressed(.w) { movement.z += 1 } - if input.isKeyPressed(.s) { movement.z -= 1 } - if input.isKeyPressed(.d) { movement.x += 1 } - if input.isKeyPressed(.a) { movement.x -= 1 } - if input.isKeyPressed(.e) { movement.y += 1 } - if input.isKeyPressed(.q) { movement.y -= 1 } - session.flight.move(local: movement, seconds: Double(time.deltaTime), fast: input.isKeyPressed(.shift)) - let next = session.flight.transform - if next != transform { - camera.components[Transform.self] = next - camera.components[GlobalTransform.self] = GlobalTransform(matrix: next.matrix) - } - if var environment = camera.components[Environment3D.self], environment.gpuOcclusionEnabled != session.hiZ { - environment.gpuOcclusionEnabled = session.hiZ - camera.components[Environment3D.self] = environment - } - } -} diff --git a/Demos/Sponza/SponzaDemoProfiler.swift b/Demos/Sponza/SponzaDemoProfiler.swift deleted file mode 100644 index 9ea0fdea2..000000000 --- a/Demos/Sponza/SponzaDemoProfiler.swift +++ /dev/null @@ -1,357 +0,0 @@ -import AdaEngine -import Foundation -import SponzaCamera - -/// Opt-in measurements of actual SceneView publication, with no image readback in the interval. -@MainActor -final class SponzaDemoProfiler { - struct Measurement: Codable { - var view: String - var shadows: Bool - var round: Int - var width: Int - var height: Int - var publishedIntervalsMilliseconds: [Double] - var graphRecords: [RenderGraphFrameRecord] - var motion: String? - var cameraUpdates: Int? - var preset: String? - var localLightCount: Int? - var lightStatistics: [String: Render3DLightStatistics.Snapshot]? - var pathTracing: [String: PathTracing3DStatus]? - var pathFramesBefore: [String: Int]? - var pathSettings: PathTracing3D? - var meshlets: [String: Meshlet3DCount]? - var meshletGPUIndexCount: Int? - var meshletLODPixelError: Float? - var shadowCascadeResolution: Int? - var localShadowResolution: Int? - var shadowDistance: Float? - } - - let output: URL - let usesFixedViewport = ProcessInfo.processInfo.environment["ADAENGINE_SPONZA_PROFILE_STUTTER"] != "1" - private(set) var isRunning = false - var recording = false - var last: ContinuousClock.Instant? - var intervals: [Double] = [] - var size = [0, 0] - var sizeChanged = false - var motion = SponzaCameraMotion.stationary - var motionOrigin = Transform() - var motionStart: ContinuousClock.Instant? - var cameraUpdates = 0 - - init(output: URL) { self.output = output } - - func cameraPose() -> Transform? { - guard motion != .stationary, let motionStart else { return nil } - let elapsed = motionStart.duration(to: .now).components - let seconds = Double(elapsed.seconds) + Double(elapsed.attoseconds) / 1e18 - return motion.pose(from: motionOrigin, seconds: seconds) - } - - func didMoveCamera() { - if recording { cameraUpdates += 1 } - } - - func didPresent(width: Int, height: Int) { - guard recording else { return } - if size[0] > 0, size != [width, height] { sizeChanged = true } - let now = ContinuousClock.now - if let last { - PerformanceStallProbe.end("SceneView.PublishGap", since: last, count: width * height) - let elapsed = last.duration(to: now).components - intervals.append(Double(elapsed.seconds) * 1000 + Double(elapsed.attoseconds) / 1e15) - } - last = now - size = [width, height] - } - - func run(session: SponzaSession, app: AppWorlds) async throws { - guard let render = app.getSubworldBuilder(by: .renderWorld), let diagnostics = render.getResource(RenderGraphDiagnostics.self) else { return } - isRunning = true - defer { isRunning = false; recording = false; motionStart = nil } - if !usesFixedViewport { - try await runStutter(session: session, app: app, diagnostics: diagnostics) - return - } - // Keep historical profiles comparable; the new LOD profile explicitly records its quality settings. - session.meshletLODError = 0 - session.highResolutionShadows = false - if ProcessInfo.processInfo.environment["ADAENGINE_SPONZA_PROFILE_MESHLETS"] == "1" - || ProcessInfo.processInfo.environment["ADAENGINE_SPONZA_PROFILE_MESHLET_LOD"] == "1" { - try await runMeshlets(session: session, app: app, diagnostics: diagnostics) - return - } - session.meshletMode = .disabled - session.meshletMapEnabled = false - if let lighting = ProcessInfo.processInfo.environment["ADAENGINE_SPONZA_PROFILE_LIGHTING"], - ["1", "lodRaster", "pathTracing"].contains(lighting) { - try await runLighting(session: session, app: app, diagnostics: diagnostics) - return - } - try await app.withWorldAccess { - session.dlssEnabled = false - session.flyingLightEnabled = false - session.apply(preset: .fullRaster, lights: 0) - } - var measurements: [Measurement] = [] - let requestedMotion = ProcessInfo.processInfo.environment["ADAENGINE_SPONZA_PROFILE_MOTION"] - let selectedMotion = requestedMotion.flatMap(SponzaCameraMotion.init(rawValue:)) - let motionRun = requestedMotion == "1" || selectedMotion != nil - let motions = selectedMotion.map { [$0] } ?? SponzaCameraMotion.allCases - let scenarios = motionRun ? motions.map { (2, $0) } : [0, 2, 3].map { ($0, .stationary) } - for (view, mode) in scenarios { - for round in 0..<3 { - for shadows in round.isMultiple(of: 2) ? [true, false] : [false, true] { - try await app.withWorldAccess { - session.move(to: view) - motion = mode - motionOrigin = session.flight.transform - motionStart = .now - if let camera = session.camera, var environment = camera.components[Environment3D.self] { - environment.shadows.isEnabled = shadows - camera.components[Environment3D.self] = environment - } - diagnostics.configure(isEnabled: true, capacity: 64) - diagnostics.clear() - } - session.status = "Измеряем сцену…" - try await Task.sleep(for: .seconds(1)) - intervals.removeAll(keepingCapacity: true) - intervals.reserveCapacity(1024) - last = nil - cameraUpdates = 0 - recording = true - try await Task.sleep(for: .seconds(motionRun ? 6 : 2)) - recording = false - let records = try await app.withWorldAccess { diagnostics.records } - let name = ["atrium", "corridor", "gallery", "outside"][view] - measurements.append(Measurement(view: name, shadows: shadows, round: round, - width: size[0], height: size[1], publishedIntervalsMilliseconds: intervals, graphRecords: records, - motion: mode.rawValue, cameraUpdates: cameraUpdates, preset: session.preset.rawValue, - localLightCount: session.lightCount)) - let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted, .sortedKeys] - try FileManager.default.createDirectory(at: output.deletingLastPathComponent(), withIntermediateDirectories: true) - try encoder.encode(measurements).write(to: output, options: .atomic) - } - } - } - try await app.withWorldAccess { - diagnostics.configure(isEnabled: false) - if let camera = session.camera, var environment = camera.components[Environment3D.self] { - environment.shadows.isEnabled = true - camera.components[Environment3D.self] = environment - } - session.move(to: 3) - } - session.status = "Sponza + Ivy · полная сцена / отдельный PT LOD" - FileHandle.standardOutput.write(Data("SPONZA_PROFILE_READY \(output.path)\n".utf8)) - } - - private func runLighting(session: SponzaSession, app: AppWorlds, diagnostics: RenderGraphDiagnostics) async throws { - let render = app.getSubworldBuilder(by: .renderWorld) - guard let tracing = render?.getResource(Render3DPathTracingStatistics.self), - let lights = render?.getResource(Render3DLightStatistics.self) else { throw CocoaError(.featureUnsupported) } - let selected = ProcessInfo.processInfo.environment["ADAENGINE_SPONZA_PROFILE_MOTION"].flatMap(SponzaCameraMotion.init(rawValue:)) ?? .rotation - session.pathHighQuality = false - session.dlssEnabled = false - session.flyingLightEnabled = false - var measurements: [Measurement] = [] - // Group modes so the cold CPU BVH/texture atlas is built once, outside measured intervals. - let requested = ProcessInfo.processInfo.environment["ADAENGINE_SPONZA_PROFILE_LIGHTING"] - let presets = requested.flatMap(SponzaRenderPreset.init(rawValue:)).map { [$0] } ?? [.lodRaster, .pathTracing] - for preset in presets { - for round in 0..<3 { - for count in round.isMultiple(of: 2) ? [0, 4, 12] : [12, 4, 0] { - motionStart = nil - try await app.withWorldAccess { - session.move(to: 2) - session.apply(preset: preset, lights: count) - } - if preset == .pathTracing { - let deadline = ContinuousClock.now + .seconds(180) - while true { - let states = tracing.snapshots.values - if let failure = states.compactMap(\.failure).first { - throw CocoaError(.featureUnsupported, userInfo: [NSLocalizedDescriptionKey: failure]) - } - if !states.isEmpty && states.allSatisfy({ $0.encodedFrames > 0 && $0.triangles > 0 && $0.lights == count + 1 }) { break } - guard ContinuousClock.now < deadline else { throw CocoaError(.executableRuntimeMismatch) } - try await Task.sleep(for: .milliseconds(50)) - } - } - try await app.withWorldAccess { - motion = selected - motionOrigin = session.flight.transform - motionStart = .now - diagnostics.configure(isEnabled: true, capacity: 64) - diagnostics.clear() - } - session.status = "Замер света: \(preset.rawValue) · \(count) источников" - try await Task.sleep(for: .seconds(1)) - let before = tracing.snapshots.mapValues(\.encodedFrames) - intervals.removeAll(keepingCapacity: true) - last = nil - cameraUpdates = 0 - recording = true - try await Task.sleep(for: .seconds(6)) - recording = false - let states = tracing.snapshots - guard !intervals.isEmpty else { throw CocoaError(.fileReadCorruptFile) } - if preset == .pathTracing { - guard !states.isEmpty && states.allSatisfy({ key, value in - value.failure == nil && value.encodedFrames > (before[key] ?? 0) && value.lights == count + 1 - }) else { - struct Failure: Encodable { - var before: [String: Int] - var after: [String: PathTracing3DStatus] - } - let data = try JSONEncoder().encode(Failure(before: before, after: states)) - try data.write(to: output.appendingPathExtension("failure.json"), options: .atomic) - let reason = "PT frame progress validation failed: \(String(decoding: data, as: UTF8.self))" - throw CocoaError(.featureUnsupported, userInfo: [NSLocalizedDescriptionKey: reason]) - } - } - let records = try await app.withWorldAccess { diagnostics.records } - measurements.append(Measurement(view: "gallery", shadows: true, round: round, - width: size[0], height: size[1], publishedIntervalsMilliseconds: intervals, graphRecords: records, - motion: selected.rawValue, cameraUpdates: cameraUpdates, preset: preset.rawValue, localLightCount: count, - lightStatistics: lights.snapshots, pathTracing: states, pathFramesBefore: before, - pathSettings: session.camera?.components[Environment3D.self]?.pathTracing)) - let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted, .sortedKeys] - try FileManager.default.createDirectory(at: output.deletingLastPathComponent(), withIntermediateDirectories: true) - try encoder.encode(measurements).write(to: output, options: .atomic) - } - } - } - motionStart = nil - try await app.withWorldAccess { - diagnostics.configure(isEnabled: false) - session.pathHighQuality = true - session.dlssEnabled = true - session.apply(preset: .pathTracing, lights: 12) - session.move(to: 2) - } - session.status = "Sponza + Ivy · полная сцена / отдельный PT LOD" - FileHandle.standardOutput.write(Data("SPONZA_PROFILE_READY \(output.path)\n".utf8)) - } - - private func runMeshlets(session: SponzaSession, app: AppWorlds, diagnostics: RenderGraphDiagnostics) async throws { - let render = app.getSubworldBuilder(by: .renderWorld) - guard let statistics = render?.getResource(Render3DMeshletStatistics.self) else { throw CocoaError(.featureUnsupported) } - session.dlssEnabled = false - session.meshletMapEnabled = false - session.flyingLightEnabled = false - session.hiZ = false - let lodRun = ProcessInfo.processInfo.environment["ADAENGINE_SPONZA_PROFILE_MESHLET_LOD"] == "1" - session.highResolutionShadows = lodRun - var measurements: [Measurement] = [] - var measuredSize: [Int]? - // The outside overview primes all eligible geometry uploads and material variants before timing. - try await app.withWorldAccess { - session.meshletMode = .gpu - session.meshletLODError = lodRun ? 2 : 0 - session.apply(preset: .fullRaster, lights: 0) - session.move(to: 3) - } - session.status = "Прогрев полной геометрии и GPU мешлетов…" - try await Task.sleep(for: .seconds(3)) - try await app.withWorldAccess { - session.move(to: 2) - motion = .rotation - motionOrigin = session.flight.transform - motionStart = .now - } - // A complete turn warms the partially clipped parts that actually enter the compute path. - try await Task.sleep(for: .seconds(6)) - for view in [2, 3] { - for round in 0..<3 { - let cases: [(mode: MeshletVisibilityMode3D, error: Float)] = lodRun - ? [(.gpu, 0), (.gpu, 2), (.gpu, 8)] : [(.disabled, 0), (.cpuReference, 0), (.gpu, 0)] - for sample in round.isMultiple(of: 2) ? cases : Array(cases.reversed()) { - let mode = sample.mode - try await app.withWorldAccess { - session.meshletMode = mode - session.meshletLODError = sample.error - session.apply(preset: .fullRaster, lights: 0) - session.move(to: view) - motion = view == 2 ? .rotation : .stationary - motionOrigin = session.flight.transform - motionStart = .now - diagnostics.configure(isEnabled: true, capacity: 32) - diagnostics.clear() - } - session.status = "Замер мешлетов · \(mode.rawValue) · LOD \(Int(sample.error)) px · \(view == 2 ? "галерея" : "снаружи") · \(round + 1)/3" - try await Task.sleep(for: .seconds(1)) - intervals.removeAll(keepingCapacity: true) - last = nil - cameraUpdates = 0 - size = [0, 0] - sizeChanged = false - recording = true - try await Task.sleep(for: .seconds(4)) - recording = false - guard !intervals.isEmpty, !sizeChanged, measuredSize == nil || measuredSize == size else { - throw CocoaError(.featureUnsupported, userInfo: [NSLocalizedDescriptionKey: "Benchmark requires a constant render size and published frames"]) - } - measuredSize = size - // Freeze frame updates only after timing, so statistics and arguments describe one completed frame. - let captured = try await app.withExclusiveWorldAccess { - let snapshot = statistics.snapshots - let actualGPUOrFullyVisible = snapshot.values.contains { - $0.gpuDraws > 0 || ($0.candidateMeshlets > 0 && $0.fullyVisibleItems > 0 && $0.fullyVisibleItems == $0.eligibleItems - && $0.fallbackReasons["processorUnavailableOrBudget"] == nil) - } - if mode == .gpu && !actualGPUOrFullyVisible { - throw CocoaError(.featureUnsupported, userInfo: [NSLocalizedDescriptionKey: "No actual GPU meshlet draws in benchmark"]) - } - if lodRun && sample.error > 0 && !snapshot.values.contains(where: { $0.lodGPUItems > 0 }) { - throw CocoaError(.featureUnsupported, userInfo: [NSLocalizedDescriptionKey: "No actual adaptive LOD draws in benchmark"]) - } - var submittedIndices: Int? - // Explicit benchmark readback AFTER the measured interval; never in ordinary frames. - if mode == .gpu, let camera = session.camera, - let output = render?.getResource(Meshlet3DState.self)?.output(for: camera.id) { - let bytes = try await output.arguments.readData() - var total = 0 - for draw in 0...stride - guard offset + 4 <= bytes.count else { throw CocoaError(.fileReadCorruptFile) } - let word = UInt32(bytes[offset]) | UInt32(bytes[offset + 1]) << 8 | UInt32(bytes[offset + 2]) << 16 | UInt32(bytes[offset + 3]) << 24 - total += Int(word) - } - submittedIndices = total - } - return (snapshot, submittedIndices, diagnostics.records) - } - measurements.append(Measurement(view: view == 2 ? "gallery" : "outside", shadows: true, round: round, - width: size[0], height: size[1], publishedIntervalsMilliseconds: intervals, graphRecords: captured.2, - motion: motion.rawValue, cameraUpdates: cameraUpdates, preset: session.preset.rawValue, localLightCount: 0, - meshlets: captured.0, meshletGPUIndexCount: captured.1, meshletLODPixelError: sample.error, - shadowCascadeResolution: session.highResolutionShadows ? 2048 : 1024, - localShadowResolution: session.highResolutionShadows ? 1024 : 512, - shadowDistance: session.highResolutionShadows ? 80 : 30)) - let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted, .sortedKeys] - try FileManager.default.createDirectory(at: output.deletingLastPathComponent(), withIntermediateDirectories: true) - try encoder.encode(measurements).write(to: output, options: .atomic) - } - } - } - motionStart = nil - try await app.withWorldAccess { - diagnostics.configure(isEnabled: false) - session.meshletMode = .gpu - session.meshletLODError = lodRun ? 2 : 0 - session.meshletMapEnabled = true - session.apply(preset: .fullRaster, lights: 0) - session.move(to: 2) - } - session.status = "Sponza + Ivy · мешлеты\(lodRun ? " + LOD" : "") · полный raster asset" - FileHandle.standardOutput.write(Data("SPONZA_PROFILE_READY \(output.path)\n".utf8)) - } -} diff --git a/Demos/Sponza/SponzaLighting.swift b/Demos/Sponza/SponzaLighting.swift deleted file mode 100644 index a9f0ba814..000000000 --- a/Demos/Sponza/SponzaLighting.swift +++ /dev/null @@ -1,134 +0,0 @@ -import AdaEngine -import Foundation -import Math -import SponzaCamera - -enum SponzaRenderPreset: String, CaseIterable { - case fullRaster - case lodRaster - case pathTracing - - var usesLOD: Bool { self != .fullRaster } -} - -@MainActor -struct SponzaLighting { - private var lamps: [Entity] = [] - private var flyingLamp: Entity? - - mutating func spawn(in world: World, parent: Entity) { - let spots = world.spawn("Gallery spotlights") { Transform() } - let points = world.spawn("Gallery fill lights") { Transform() } - let moving = world.spawn("Flying test light") { Transform() } - parent.addChild(spots) - parent.addChild(points) - parent.addChild(moving) - let lamp = world.spawn("Moving point light") { - PointLightComponent(radiance: [0.85, 0.93, 1], intensity: 25, castShadows: true, range: 10) - Transform(position: SponzaLightMotion.position(seconds: 0)) - } - moving.addChild(lamp) - flyingLamp = lamp - for (index, x) in [Float(-9), -3, 3, 9].enumerated() { - let position = Vector3(x, 8.4, 5.35) - let lamp = world.spawn("Spot \(index + 1)") { - SpotLightComponent(radiance: [1, 0.92, 0.82], intensity: 8, castShadows: true, - range: 9, innerConeAngle: 25, outerConeAngle: 42) - SponzaFlyCamera.looking(from: position, at: [x, 6.4, 5.35]) - } - spots.addChild(lamp) - lamps.append(lamp) - } - for z in [Float(-5.35), 5.35] { - for (index, x) in [Float(-9), -3, 3, 9].enumerated() { - let lamp = world.spawn("Fill \(lamps.count - 3)") { - PointLightComponent(radiance: index.isMultiple(of: 2) ? [1, 0.9, 0.8] : [0.8, 0.9, 1], - intensity: 2, castShadows: false, range: 8) - Transform(position: [x, 7.6, z]) - } - points.addChild(lamp) - lamps.append(lamp) - } - } - } - - func setCount(_ count: Int) { - for (index, lamp) in lamps.enumerated() { - lamp.components[Visibility.self] = index < count ? .visible : .hidden - } - } - - func updateFlight(enabled: Bool, seconds: Double) { - guard let flyingLamp else { return } - let visibility: Visibility = enabled ? .visible : .hidden - if flyingLamp.components[Visibility.self] != visibility { flyingLamp.components[Visibility.self] = visibility } - guard enabled else { return } - let pose = Transform(position: SponzaLightMotion.position(seconds: seconds)) - flyingLamp.components[Transform.self] = pose - flyingLamp.components[GlobalTransform.self] = GlobalTransform(matrix: pose.matrix) - } -} - -extension SponzaSession { - func apply(preset: SponzaRenderPreset, lights: Int) { - fullGeometry?.components[Visibility.self] = preset.usesLOD ? .hidden : .visible - lodGeometry?.components[Visibility.self] = preset.usesLOD ? .visible : .hidden - lighting.setCount(lights) - lighting.updateFlight(enabled: flyingLightEnabled, seconds: flightSeconds) - if let sun, var light = sun.components[DirectionalLightComponent.self] { - light.shadowDistance = highResolutionShadows ? 80 : 30 - sun.components[DirectionalLightComponent.self] = light - } - if let camera, var component = camera.components[Camera.self] { - component.temporalUpscaling = dlssEnabled && dlssAvailable && !meshletMapEnabled - ? .init(renderScale: pathHighQuality ? 2.0 / 3.0 : 0.5, method: .automatic) : nil - camera.components[Camera.self] = component - } - if let camera, var environment = camera.components[Environment3D.self] { - environment.pathTracing = PathTracing3D(isEnabled: preset == .pathTracing, - samplesPerPixel: 1, maximumBounces: 2, - resolutionScale: dlssEnabled && dlssAvailable ? 1 : (pathHighQuality ? 0.5 : 0.25), historyLimit: 128) - environment.pathTracing.textureTileSize = 512 - environment.debugView = meshletMapEnabled ? .meshlets : .none - environment.meshVisibility.meshlets = preset == .pathTracing ? .disabled : meshletMode - environment.meshVisibility.meshletLODPixelError = meshletLODError - environment.shadows.isEnabled = true - environment.shadows.resolution = highResolutionShadows ? 2048 : 1024 - environment.shadows.splitLambda = highResolutionShadows ? 0.85 : 0.65 - environment.localShadows = LocalShadowSettings3D(isEnabled: true, maximumLights: 4, resolution: highResolutionShadows ? 1024 : 512) - camera.components[Environment3D.self] = environment - } - self.preset = preset - self.lightCount = lights - rendererStatus = preset == .pathTracing ? "Path tracing · 1 spp · 2 отскока · подготовка BVH…" - : "\(preset.usesLOD ? "PT LOD" : "Полная геометрия") · растр · \(lights) локальных источников" - } - - func refreshPathStatus() { - guard ContinuousClock.now >= nextStatusUpdate else { return } - nextStatusUpdate = .now + .seconds(1) - if preset == .pathTracing && !meshletMapEnabled { - meshletStatus = "Мешлеты — растровый путь; PT использует свой BVH" - } else if let status = meshletStatistics?.snapshots.values.first { - let levels = status.selectedLODLevels.keys.sorted().map { "L\($0): \(status.selectedLODLevels[$0] ?? 0)" }.joined(separator: " / ") - let lod = meshletLODError > 0 - ? (meshletMapEnabled ? "\n\(levels.isEmpty ? "LOD: нет данных" : levels) · \(status.lodSelectedTriangles) треуг." - : " · GPU LOD: \(status.lodGPUItems)") : "" - meshletStatus = meshletMapEnabled - ? "Кластеры \(status.candidateMeshlets) · серый = без карты (\(status.fallbackItems))\(lod)" - : "Кластеры \(status.candidateMeshlets) · GPU \(status.gpuDraws) · целиком \(status.fullyVisibleItems) · fallback \(status.fallbackItems)\(lod)" - } - if meshletMapEnabled { dlssStatus = "DLSS: отключён для карты мешлетов" } - else if dlssEnabled && dlssAvailable, let status = temporalStatistics?.snapshots.values.first { - dlssStatus = "DLSS · \(status.inputWidth)×\(status.inputHeight) → \(status.outputWidth)×\(status.outputHeight)" - + " · кадры \(status.encodedFrames) · fallback \(status.fallbackFrames)" - } else { dlssStatus = dlssAvailable ? "DLSS: выкл." : "DLSS недоступен на текущем устройстве/сборке" } - guard preset == .pathTracing, let status = pathStatistics?.snapshots.values.first else { return } - if let failure = status.failure { - rendererStatus = "Path tracing недоступен: \(failure)" - } else { - rendererStatus = "Path tracing · \(status.triangles) треуг. · \(status.lights) источников · \(status.width)×\(status.height)" - + " · кадр \(status.encodedFrames) · накопление \(status.accumulatedFrames)" - } - } -} diff --git a/Demos/Sponza/SponzaStutterProfiler.swift b/Demos/Sponza/SponzaStutterProfiler.swift deleted file mode 100644 index 9ca17ab9d..000000000 --- a/Demos/Sponza/SponzaStutterProfiler.swift +++ /dev/null @@ -1,72 +0,0 @@ -import AdaEngine -import Foundation -import SponzaCamera - -extension SponzaDemoProfiler { - struct StutterPhase: Codable { - var name: String - var input: String - var width: Int - var height: Int - var sizeChanged: Bool - var publishedIntervalsMilliseconds: [Double] - var graphRecords: [RenderGraphFrameRecord] - var meshlets: [String: Meshlet3DCount] - } - - func runStutter(session: SponzaSession, app: AppWorlds, diagnostics: RenderGraphDiagnostics) async throws { - let render = app.getSubworldBuilder(by: .renderWorld) - let statistics = render?.getResource(Render3DMeshletStatistics.self) - var captured: [StutterPhase] = [] - session.status = "Sponza + Ivy · запись задержек кадров" - let cases: [(String, SponzaCameraMotion, Double)] = [ - ("stationary", .stationary, 6), ("rotation-first", .rotation, 12), ("rotation-repeat", .rotation, 12), - ("translation", .translation, 12), ("LOD-off", .rotation, 6), ("LOD-on", .rotation, 6), - ("DLSS-off", .rotation, 6), ("DLSS-on", .rotation, 6), - ("shadows-baseline", .stationary, 4), ("shadows-high", .stationary, 4), - ("lights-off", .stationary, 4), ("lights-on", .stationary, 4) - ] - for (name, movement, seconds) in cases { - try await app.withWorldAccess { - motionStart = nil - session.meshletMode = .gpu - session.meshletLODError = name == "LOD-off" ? 0 : 2 - session.meshletMapEnabled = false - session.flyingLightEnabled = false - session.dlssEnabled = name != "DLSS-off" - session.highResolutionShadows = name != "shadows-baseline" - session.apply(preset: .fullRaster, lights: name == "lights-off" ? 0 : 12) - session.move(to: 2) - motion = movement - motionOrigin = session.flight.transform - motionStart = .now - diagnostics.configure(isEnabled: true, capacity: 4096) - diagnostics.clear() - } - intervals.removeAll(keepingCapacity: true) - last = nil - size = [0, 0] - sizeChanged = false - cameraUpdates = 0 - PerformanceStallProbe.mark("begin:\(name)") - recording = true - try await Task.sleep(for: .seconds(seconds)) - recording = false - PerformanceStallProbe.mark("end:\(name)") - let data = try await app.withExclusiveWorldAccess { (diagnostics.records, statistics?.snapshots ?? [:]) } - captured.append(.init(name: name, input: movement.rawValue, width: size[0], height: size[1], sizeChanged: sizeChanged, - publishedIntervalsMilliseconds: intervals, graphRecords: data.0, meshlets: data.1)) - let encoder = JSONEncoder() - encoder.outputFormatting = [.sortedKeys] - try FileManager.default.createDirectory(at: output.deletingLastPathComponent(), withIntermediateDirectories: true) - try encoder.encode(captured).write(to: output, options: .atomic) - } - try await app.withWorldAccess { - diagnostics.configure(isEnabled: false) - motionStart = nil - session.move(to: 2) - } - session.status = "Sponza + Ivy · запись задержек завершена" - print("SPONZA_STUTTER_READY \(output.path)") - } -} diff --git a/Demos/Sponza/SponzaWarmup.swift b/Demos/Sponza/SponzaWarmup.swift deleted file mode 100644 index 2999ef09f..000000000 --- a/Demos/Sponza/SponzaWarmup.swift +++ /dev/null @@ -1,111 +0,0 @@ -import AdaEngine -import Foundation - -enum SponzaWarmupError: Error, CustomStringConvertible { - case alreadyWaiting - case timedOut(String) - - var description: String { - switch self { - case .alreadyWaiting: "Прогрев уже ожидает кадры" - case .timedOut(let stage): "Не получены готовые кадры прогрева: \(stage)" - } - } -} - -/// A demo-local barrier driven by actual SceneView publications, not elapsed warmup time. -@MainActor -final class SponzaWarmupFrames { - private struct Waiter { - let id: UUID - let target: Int - let condition: @MainActor () -> Bool - let continuation: CheckedContinuation - let deadline: Task - } - private var publications = 0 - private var waiter: Waiter? - - func didPublish() { - publications += 1 - guard let waiter, publications >= waiter.target, waiter.condition() else { return } - finish(waiter.id) - } - - func wait(stage: String, frames: Int = 6, timeout: Duration = .seconds(60), - condition: @escaping @MainActor () -> Bool = { true }) async throws { - guard waiter == nil else { throw SponzaWarmupError.alreadyWaiting } - let id = UUID() - try Task.checkCancellation() - try await withTaskCancellationHandler { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - let deadline = Task { @MainActor [weak self] in - do { try await Task.sleep(for: timeout) } catch { return } - self?.finish(id, error: SponzaWarmupError.timedOut(stage)) - } - waiter = Waiter(id: id, target: publications + frames, condition: condition, continuation: continuation, deadline: deadline) - } - } onCancel: { - Task { @MainActor [weak self] in self?.finish(id, error: CancellationError()) } - } - } - - private func finish(_ id: UUID, error: (any Error)? = nil) { - guard let pending = waiter, pending.id == id else { return } - waiter = nil - pending.deadline.cancel() - if let error { pending.continuation.resume(throwing: error) } else { pending.continuation.resume() } - } -} - -extension SponzaSession { - /// Render both depth-format pipeline variants while controls remain in the loading state. - func warmupRaster(app: AppWorlds) async throws { - let selectedPreset = preset - let selectedDLSS = dlssEnabled - let selectedMap = meshletMapEnabled - let selectedMeshlets = meshletMode - let selectedLODError = meshletLODError - let selectedFlight = flyingLightEnabled - let selectedLights = lightCount - @MainActor func restoreSelection() { - dlssEnabled = selectedDLSS - meshletMapEnabled = selectedMap - meshletMode = selectedMeshlets - meshletLODError = selectedLODError - flyingLightEnabled = selectedFlight - apply(preset: selectedPreset, lights: selectedLights) - } - do { - let variants = dlssAvailable ? [false, true] : [false] - for temporal in variants { - try Task.checkCancellation() - let stage = temporal ? "растр · DLSS вкл." : "растр · DLSS выкл." - status = "Прогреваем \(stage)…" - try await app.withWorldAccess { - dlssEnabled = temporal - meshletMapEnabled = false - // Prime the exact leaf stream as well as the selected hierarchy below. Otherwise - // turning LOD off first uploads Ivy's large v1 stream during camera movement. - meshletMode = selectedMeshlets == .gpu ? .gpu : .disabled - meshletLODError = 0 - flyingLightEnabled = false - apply(preset: .fullRaster, lights: selectedLights) - } - // Six publications exceed the three in-flight slots. The DLSS stage additionally - // requires encoded temporal frames; an empty/fallback-only image cannot prove warmup. - try await warmupFrames.wait(stage: stage) { [weak self] in - guard let snapshots = self?.temporalStatistics?.snapshots else { return true } - return temporal ? snapshots.values.contains { $0.encodedFrames >= 3 && $0.fallbackFrames == 0 } : snapshots.isEmpty - } - FileHandle.standardOutput.write(Data("SPONZA_WARMUP_READY \(temporal ? "temporal" : "native")\n".utf8)) - } - status = "Готовим выбранный режим…" - try await app.withWorldAccess { restoreSelection() } - try await warmupFrames.wait(stage: "выбранный режим") - } catch { - try? await app.withWorldAccess { restoreSelection() } - throw error - } - } -} diff --git a/Demos/Sponza/Tests/SponzaCameraTests.swift b/Demos/Sponza/Tests/SponzaCameraTests.swift deleted file mode 100644 index 0ef7dbdbb..000000000 --- a/Demos/Sponza/Tests/SponzaCameraTests.swift +++ /dev/null @@ -1,66 +0,0 @@ -import AdaTransform -import Testing -@testable import SponzaCamera - -@Suite("Sponza fly camera") -struct SponzaCameraTests { - @Test func yawTurnsHorizontallyAndPitchVerticallyWithoutRoll() { - let yaw = Transform(rotation: SponzaFlyCamera.rotation(yaw: .pi / 2, pitch: 0)).matrix - #expect(abs(yaw.z.x - 1) < 0.00001 && abs(yaw.z.y) < 0.00001 && abs(yaw.z.z) < 0.00001) - let pitch = Transform(rotation: SponzaFlyCamera.rotation(yaw: 0, pitch: .pi / 4)).matrix - #expect(pitch.z.y < -0.7 && pitch.z.z > 0.7 && abs(pitch.z.x) < 0.00001) - let mixed = Transform(rotation: SponzaFlyCamera.rotation(yaw: 1.2, pitch: 0.6)).matrix - #expect(abs(mixed.x.y) < 0.00001, "Camera right stays horizontal; there is no bank") - } - - @Test func resetsPreservePoseAndMouseMotionDoesNotJumpAxes() { - let pose = SponzaFlyCamera.looking(from: [-32, 25, -35], at: [0, 4, 0]) - var camera = SponzaFlyCamera() - camera.setPose(pose) - camera.look(delta: .zero) - #expect(camera.transform == pose) - camera.look(delta: [1, 0]) - #expect(abs(camera.transform.matrix.z.y - pose.matrix.z.y) < 0.00001) - camera.look(delta: [0, 10000]) - #expect(camera.pitch <= 1.55) - } - - @Test func movementHasConstantDiagonalSpeedAndUsesCameraForward() { - var straight = SponzaFlyCamera(), diagonal = SponzaFlyCamera() - straight.move(local: [0, 0, 1], seconds: 0.05, fast: false) - diagonal.move(local: [1, 0, 1], seconds: 0.05, fast: false) - #expect(abs(straight.transform.position.length - diagonal.transform.position.length) < 0.00001) - var turned = SponzaFlyCamera() - turned.setPose(Transform(rotation: SponzaFlyCamera.rotation(yaw: .pi / 2, pitch: 0))) - turned.move(local: [0, 0, 1], seconds: 0.05, fast: false) - #expect(abs(turned.transform.position.x - 0.15) < 0.00001 && abs(turned.transform.position.z) < 0.00001) - } - - @Test func galleryMotionIsPeriodicAndKeepsTranslationInsideTheCorridor() { - let origin = SponzaFlyCamera.looking(from: [-9.594356, 6.921101, 5.35278], at: [0, 6.921101, 5.35278]) - for mode in SponzaCameraMotion.allCases { - #expect(mode.pose(from: origin, seconds: 0) == origin) - #expect(mode.pose(from: origin, seconds: 6) == origin) - #expect(mode.pose(from: origin, seconds: .nan) == origin) - } - let midpoint = SponzaCameraMotion.translation.pose(from: origin, seconds: 3) - #expect(abs(midpoint.position.x - origin.position.x - 3) < 0.00001) - #expect(midpoint.position.y == origin.position.y && midpoint.position.z == origin.position.z) - #expect(midpoint.rotation == origin.rotation) - let turned = SponzaCameraMotion.rotation.pose(from: origin, seconds: 1.5) - #expect(turned.position == origin.position) - #expect(abs(turned.matrix.x.y) < 0.00001) - #expect(turned.rotation != origin.rotation) - } - - @Test func flyingLightStaysInGalleryAndCompletesItsOrbit() { - let start = SponzaLightMotion.position(seconds: 0) - #expect((SponzaLightMotion.position(seconds: 12) - start).length < 0.0001) - #expect(SponzaLightMotion.position(seconds: .nan) == start) - #expect((SponzaLightMotion.position(seconds: 3) - start).length > 8) - for step in 0...120 { - let p = SponzaLightMotion.position(seconds: Double(step) / 10) - #expect(abs(p.x) <= 8 && p.y >= 6.8 && p.y <= 8 && p.z >= 4.85 && p.z <= 5.85) - } - } -} diff --git a/Demos/SpriteInstancingValidation/README.md b/Demos/SpriteInstancingValidation/README.md deleted file mode 100644 index d6e263f06..000000000 --- a/Demos/SpriteInstancingValidation/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Sprite instancing validation - -Separate QA app using production SpriteRenderSystem / SpriteDrawPass / shaders. -The same 640x360 GPU fixture covers stretch/fit/fill, sliced/tiled atlas regions, -flips, reflected/rotated transforms, anchors, alpha and ordered texture changes. -It compares readback pixels for five expanded and five instanced frames, with a -1200-sprite contiguous run. A PASS requires identical nonempty pixels and active -instancing. It reports backend, quads and draw count; no backend fallback counts as proof. - -iOS, from this demo directory: - -```sh -xcodegen generate --spec project.yml -``` - -Build and launch the scheme with `ADAENGINE_DISABLE_SWAN=1` in the host environment. -On iOS the visible PASS also writes `Documents/sprite-validation.json` in the QA -app's container, allowing a physical device result to be retrieved without relying -on launch success alone. - -Browser, from the engine root: - -```sh -python3 script/ensure_tint.py -swift package --allow-writing-to-package-directory export-web \ - --product SpriteInstancingValidation --output dist/SpriteInstancingValidation \ - --scratch-path /tmp/ada-sprite-web-validation \ - --swift-sdk swift-6.3.2-RELEASE_wasm --debug -``` - -Use compatible Swan/Dawn; local development overrides are described in -[Tools/Tint](../../Tools/Tint/README.md). Serve the output over HTTP and inspect the -console `[SpriteValidation] PASS`. The canvas presents the validated GPU readback -image with a green border; failures log FAIL and show a red background. Known color -probes reject a blank frame even when both rendering modes produce equal bytes. - -Recorded proof: macOS native WebGPU, actual WASM WebGPU, iPhone 17 Pro Simulator, -and physical iPad8,1 / iPadOS 26.5. Rendering correctness is verified; this app -provides no browser/mobile performance benchmark or Editor UI validation. diff --git a/Demos/SpriteInstancingValidation/SpriteValidationApp.swift b/Demos/SpriteInstancingValidation/SpriteValidationApp.swift deleted file mode 100644 index 5ac1fc6c4..000000000 --- a/Demos/SpriteInstancingValidation/SpriteValidationApp.swift +++ /dev/null @@ -1,152 +0,0 @@ -import AdaApp -import AdaCorePipelines -import AdaECS -import AdaRender -import AdaSprite -import AdaText -import AdaTransform -import AdaUtils -import Foundation -import Math - -#if os(iOS) - import SwiftUI - import UIKit - - /// Separate QA application; no validation hooks are installed in Studio. - @main - struct SpriteValidationApp: SwiftUI.App { - var body: some SwiftUI.Scene { WindowGroup { ValidationView() } } - } - - private struct ValidationView: SwiftUI.View { - @State private var status = "Running Metal A/B readback…" - @State private var image: UIImage? - @State private var passed = false - var body: some SwiftUI.View { - VStack(spacing: 20) { - Text("Sprite instancing").font(.title) - Text(status).foregroundStyle(passed ? .green : .primary).accessibilityIdentifier("validation-status") - if let image { SwiftUI.Image(uiImage: image).resizable().interpolation(.none).aspectRatio(contentMode: .fit) } - } - .padding() - .task { - do { - unsafe RenderEngine.configurations.preferredBackend = .metal - let host = AppWorlds(main: World()) - host.addPlugin(RenderWorldPlugin()) - try await host.build() - let result = try await SpriteValidationFixture.run() - let bytes = result.image.data as CFData - guard let provider = CGDataProvider(data: bytes), - let cgImage = CGImage( - width: 640, - height: 360, - bitsPerComponent: 8, - bitsPerPixel: 32, - bytesPerRow: 640 * 4, - space: CGColorSpaceCreateDeviceRGB(), - bitmapInfo: result.image.format == .bgra8 - ? [.byteOrder32Little, CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)] - : [.byteOrder32Big, CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)], - provider: provider, - decode: nil, - shouldInterpolate: false, - intent: .defaultIntent - ) - else { return } - let report: [String: Any] = [ - "passed": true, "backend": String(describing: result.backend), "frames": 10, - "quads": result.quads, "draws": result.draws, "pixels": "identical", "controlPixels": "passed", - ] - let documents = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) - try JSONSerialization.data(withJSONObject: report, options: [.prettyPrinted, .sortedKeys]) - .write(to: documents.appendingPathComponent("sprite-validation.json"), options: .atomic) - image = UIImage(cgImage: cgImage) - status = "PASS · Metal · identical pixels\n10 frames · \(result.quads) quads · \(result.draws) draws" - passed = true - } catch { - status = "FAIL: \(error)" - print("[SpriteValidation] FAIL: \(error)") - } - } - } - } -#else - import AdaAssets - import AdaInput - import AdaPlatform - import AdaUI - - @main - struct SpriteValidationApp: AdaApp.App { - var body: some AppScene { - EmptyWindow().transformAppWorlds { world in - world.insertPlugin(ValidationPlugin(), after: MainSchedulerPlugin.self) - } - .window(with: UIWindow.Configuration(title: "Sprite instancing validation", frame: Rect(x: 80, y: 80, width: 960, height: 540))) - } - } - - private struct ValidationPlugin: Plugin { - func setup(in app: AppWorlds) { - app.addPlugin(TransformPlugin()) - app.addPlugin(AppPlatformPlugin()) - app.addPlugin(InputPlugin()) - app.addPlugin(RenderWorldPlugin()) - app.addPlugin(CameraPlugin()) - app.addPlugin(AssetsPlugin()) - app.addPlugin(VisibilityPlugin()) - app.addPlugin(SpritePlugin()) - app.addPlugin(Mesh2DPlugin()) - app.addPlugin(TextPlugin()) - app.addPlugin(WindowPlugin()) - app.addPlugin(Core2DPlugin()) - app.addPlugin(UpscalePlugin()) - app.addPlugin(UIPlugin()) - app.addSystem(StartupSpriteValidationSystem.self, on: .startup) - } - } - - @System - @MainActor - func StartupSpriteValidation(_ context: WorldUpdateContext) async { - do { - let result = try await SpriteValidationFixture.run() - context.world.spawn { - Sprite(texture: Texture2D(image: result.image), size: [640, 360]) - Transform() - } - var camera = Camera() - camera.projection = .custom(ValidationProjection()) - camera.backgroundColor = .green - context.world.spawn { - camera - Transform() - GlobalViewUniform() - VisibleEntities() - Visibility.visible - CameraRenderGraph(subgraphLabel: .main2D, inputSlot: Main2DRenderNode.InputNode.view) - } - } catch { - print("[SpriteValidation] FAIL: \(error)") - var camera = Camera() - camera.backgroundColor = .red - context.world.spawn { - camera - Transform() - GlobalViewUniform() - VisibleEntities() - Visibility.visible - CameraRenderGraph(subgraphLabel: .main2D, inputSlot: Main2DRenderNode.InputNode.view) - } - } - } - - private struct ValidationProjection: CameraProjection { - var near: Float { -1 } - var far: Float { 1000 } - func makeClipView() -> Transform3D { Transform3D(scale: [Float(2) / 680, Float(2) / 400, 0.001]) } - mutating func updateView(width: Float, height: Float) {} - } -#endif diff --git a/Demos/SpriteInstancingValidation/SpriteValidationFixture.swift b/Demos/SpriteInstancingValidation/SpriteValidationFixture.swift deleted file mode 100644 index bf3050b0e..000000000 --- a/Demos/SpriteInstancingValidation/SpriteValidationFixture.swift +++ /dev/null @@ -1,154 +0,0 @@ -import AdaCorePipelines -import AdaECS -@_spi(Internal) import AdaRender -import AdaSprite -import AdaTransform -import AdaUtils -import Foundation -import Math - -/// Shared GPU readback fixture for native/mobile Metal and browser WebGPU. -@MainActor -enum SpriteValidationFixture { - struct Result { - let image: Image - let backend: RenderBackendType - let draws: Int - let quads: Int - } - - enum Failure: Error { - case unsupportedBackend, missingResource, missingImage, unequalPixels, emptyImage, missingInstancing - } - - static func run() async throws -> Result { - let device = unsafe RenderEngine.shared.renderDevice - guard device.supportsInstancedVertexInputs else { throw Failure.unsupportedBackend } - Camera.registerComponent() - let world = World() - world.insertResource(SpriteDrawPass()) - world.insertResource(SpriteBatches()) - world.insertResource(AdditionalExtractedSprites()) - world.insertResource(RenderDeviceHandler(renderDevice: device)) - world.insertResource(RenderPipelines(configurator: SpriteRenderPipeline())) - world.addSystem(SpriteRenderSystem.self, on: .update) - let view = world.spawn { Camera() } - let target = RenderTexture(size: [640, 360], scaleFactor: 1, format: .bgra8) - var pipelines = RenderPipelines(configurator: SpriteRenderPipeline()) - let pipeline = pipelines.pipeline(device: device) - var sprites: [Entity.ID: ExtractedSprite] = [:] - var items: [Transparent2DRenderItem] = [] - var atlasImage = Image(width: 32, height: 16, color: .green) - atlasImage.samplerDescription.magFilter = .nearest - atlasImage.samplerDescription.minFilter = .nearest - for y in 0..<16 { - for x in 0..<32 where x < 4 || x >= 28 || y < 4 || y >= 12 { - atlasImage.setPixel(in: [Float(x), Float(y)], color: y < 8 ? (x < 16 ? .red : .yellow) : (x < 16 ? .blue : .white)) - } - } - let atlas = TextureAtlas(from: atlasImage, size: [32, 16]) - guard let slice = atlas.textureSlice(in: RectInt(x: 0, y: 0, width: 32, height: 16)) else { throw Failure.missingImage } - let alphaTexture = Texture2D(image: Image(width: 2, height: 1, data: Data([255, 255, 255, 0, 255, 255, 255, 128]))) - func add( - texture: Texture2D, - position: Vector3, - size: Size, - mode: SpriteImageMode = .stretch, - anchor: SpriteAnchor = .center, - flipX: Bool = false, - flipY: Bool = false, - color: Color = .white, - scale: Vector3 = [1, 1, 1], - rotation: Quat = .identity - ) { - let id = sprites.count - let transform = Transform(rotation: rotation, scale: scale, position: position) - sprites[id] = ExtractedSprite( - entityId: id, - texture: texture, - size: size, - flipX: flipX, - flipY: flipY, - tintColor: color, - transform: transform, - worldTransform: transform.matrix, - anchor: anchor, - imageMode: mode - ) - items.append(.init(entity: id, drawPass: SpriteDrawPass(), renderPipeline: pipeline, sortKey: Float(id), batchRange: 0..<0)) - } - let modes: [SpriteImageMode] = [.stretch, .fit, .fill, .sliced(SpriteSliceBorder(4)), .sliced(SpriteSliceBorder(4)), .tiled()] - for (index, mode) in modes.enumerated() { - add( - texture: slice, - position: [Float(index) * 96 - 240, 112, 0], - size: [72, 72], - mode: mode, - flipX: index == 4, - flipY: index == 5 - ) - } - add(texture: .whiteTexture, position: [-280, 8, 0], size: [40, 40], anchor: .bottomLeft, color: .red) - add(texture: slice, position: [-170, 28, 0], size: [96, 48], scale: [-1, 1, 1], rotation: Quat(axis: [0, 0, 1], angle: 0.2)) - add(texture: .whiteTexture, position: [-32, 28, 0], size: [72, 48], color: .blue) - add(texture: alphaTexture, position: [-32, 28, 0], size: [72, 48]) - add(texture: .whiteTexture, position: [64, 28, 0], size: [72, 48], color: .red) - add(texture: alphaTexture, position: [64, 28, 0], size: [72, 48]) - // A long contiguous run exercises instance offsets and buffer reuse across frames. - for index in 0..<1200 { - add( - texture: .whiteTexture, - position: [Float(index % 120) * 5 - 298, Float(index / 120) * 5 - 120, 0], - size: [3, 3], - color: index % 2 == 0 ? .green : .yellow - ) - } - world.insertResource(ExtractedSprites(sprites: SparseSet(sprites))) - var reference: Data? - var resultImage: Image? - var draws = 0 - var quads = 0 - for mode in [SpriteRenderingMode.batched, .automatic] { - var data = SpriteDrawData.defaultValue - data.renderingMode = mode - world.insertResource(data) - for _ in 0..<5 { - world.insertResource(SortedRenderItems(items: RenderItems(items: items))) - await world.runScheduler(.update) - guard let prepared = world.getResource(SpriteDrawData.self), - let sorted = world.getResource(SortedRenderItems.self) - else { throw Failure.missingResource } - if mode == .automatic, !prepared.vertexBuffer.isEmpty { throw Failure.missingInstancing } - draws = sorted.items.items.filter { $0.batchRange?.isEmpty == false }.count - quads = world.getResource(SpriteBatches.self)?.batches.values.reduce(0) { $0 + $1.range.count } ?? 0 - let command = device.createCommandQueue().makeCommandBuffer() - let encoder = command.beginRenderPass( - RenderPassDescriptor(colorAttachments: [ - .init(texture: target, operation: OperationDescriptor(loadAction: .clear, storeAction: .store), clearColor: .black) - ]) - ) - let projection = Transform3D(scale: [Float(2) / 640, Float(2) / 360, 0.001]) - encoder.setVertexBuffer(GlobalViewUniform(projectionMatrix: projection, viewProjectionMatrix: projection), slot: GlobalBufferIndex.viewUniform) - encoder.setViewport(Rect(x: 0, y: 0, width: 640, height: 360)) - try sorted.items.render(with: encoder, world: world, view: view) - encoder.endRenderPass() - await withCheckedContinuation { (continuation: CheckedContinuation) in - command.addCompletedHandler { continuation.resume() } - command.commit() - } - guard let image = try await device.readImage(from: target) else { throw Failure.missingImage } - if let reference, image.data != reference { throw Failure.unequalPixels } - reference = image.data - resultImage = image - } - } - guard let image = resultImage else { throw Failure.missingImage } - guard image.getPixel(x: 50, y: 150) == .red, image.getPixel(x: 260, y: 150) == .blue, - image.getPixel(x: 356, y: 150) == .red, image.getPixel(x: 22, y: 300) == .green, - quads >= 1200, draws > 1 - else { throw Failure.emptyImage } - let backend = unsafe RenderEngine.shared.type - print("[SpriteValidation] PASS backend=\(backend) pixels=identical frames=10 quads=\(quads) draws=\(draws) instancing=true") - return Result(image: image, backend: backend, draws: draws, quads: quads) - } -} diff --git a/Demos/SpriteInstancingValidation/project.yml b/Demos/SpriteInstancingValidation/project.yml deleted file mode 100644 index 8da0860f8..000000000 --- a/Demos/SpriteInstancingValidation/project.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: SpriteInstancingValidation -options: - deploymentTarget: - iOS: '18.0' -packages: - AdaEngine: - path: ../.. -settings: - base: - SWIFT_VERSION: '6.2' - DEVELOPMENT_TEAM: 8PYCRS3EA3 - CODE_SIGN_STYLE: Automatic -targets: - SpriteInstancingValidation: - type: application - platform: iOS - sources: - - SpriteValidationApp.swift - - SpriteValidationFixture.swift - dependencies: - - package: AdaEngine - product: AdaApp - - package: AdaEngine - product: AdaCorePipelines - - package: AdaEngine - product: AdaECS - - package: AdaEngine - product: AdaRender - - package: AdaEngine - product: AdaSprite - - package: AdaEngine - product: AdaTransform - - package: AdaEngine - product: AdaUtils - - package: AdaEngine - product: Math - settings: - base: - PRODUCT_BUNDLE_IDENTIFIER: org.adaengine.SpriteInstancingValidation - GENERATE_INFOPLIST_FILE: YES - INFOPLIST_KEY_UILaunchScreen_Generation: YES - TARGETED_DEVICE_FAMILY: '1,2' - scheme: - testTargets: [] diff --git a/Documentation/A2UI.md b/Documentation/A2UI.md index 9811b53fd..c98d01ff6 100644 --- a/Documentation/A2UI.md +++ b/Documentation/A2UI.md @@ -6,7 +6,7 @@ no dependency on Compose, an LLM SDK, an agent framework, or a networking client ## Run the first experiment -From this worktree on macOS: +From this worktree on macOS: Set `ADAEXAMPLES_PATH` to your AdaExamples checkout. ```sh ./script/build_and_run.sh @@ -16,7 +16,7 @@ From this worktree on macOS: The Run action launches **A2UIFormDemo** as an application bundle. The script uses `/private/tmp/adaengine-a2ui-build` by default; set `ADAENGINE_A2UI_BUILD_PATH` to choose another task-owned cache. Logs and proof artifacts live in -`Demos/A2UIForm/dist/`. `--debug`, `--logs`, and `--telemetry` are also supported. +`Examples/Technical/A2UIForm/dist/`. `--debug`, `--logs`, and `--telemetry` are also supported. The demo streams a local agent fixture in arbitrary byte chunks, progressively renders an NPC form, and binds its name field and toggle to a surface model. diff --git a/Documentation/Linux.md b/Documentation/Linux.md index 267881bb3..cbb8d70a4 100644 --- a/Documentation/Linux.md +++ b/Documentation/Linux.md @@ -48,7 +48,8 @@ python3 script/prepare_swan_linux.py "$HOME/adaengine-linux-tools/swan-source" " export ADAENGINE_SWAN_PACKAGE_PATH="$HOME/adaengine-linux-tools/swan" export ADAENGINE_TINT_CACHE="$HOME/adaengine-linux-tools/tint" export TINT_EXECUTABLE="$(python3 script/ensure_tint.py)" -swift run --scratch-path "$ADAENGINE_LINUX_SCRATCH" -j 6 LinuxSmoke +# ADAEXAMPLES_PATH points to a clone of https://github.com/AdaEngine/AdaExamples +swift run --package-path "$ADAEXAMPLES_PATH/Examples/Technical/LinuxSmoke" --scratch-path "$ADAENGINE_LINUX_SCRATCH" -j 6 LinuxSmoke ``` The checkout used for this port is Swan commit diff --git a/Documentation/Performance/SponzaLightingPerformance-2026-11-03.md b/Documentation/Performance/SponzaLightingPerformance-2026-11-03.md index 9cef98b77..9a320e31f 100644 --- a/Documentation/Performance/SponzaLightingPerformance-2026-11-03.md +++ b/Documentation/Performance/SponzaLightingPerformance-2026-11-03.md @@ -105,7 +105,7 @@ the final benchmark. Earlier history-test logs predate the complete backpressure regression and are retained only as diagnostic history. Reproduce asset preparation and demo profiling with the instructions in -[Demos/Sponza/README.md](../../Demos/Sponza/README.md). Set +[Examples/Technical/Sponza/README.md](https://github.com/AdaEngine/AdaExamples/tree/main/Examples/Technical/Sponza/README.md). Set `ADAENGINE_SPONZA_PROFILE_LIGHTING=1`, `ADAENGINE_SPONZA_PROFILE_MOTION=rotation`, `ADAENGINE_SPONZA_RENDER_PRESET=lodRaster` and an absolute diff --git a/Documentation/Physics2D.md b/Documentation/Physics2D.md index e9c435429..b5a8a2def 100644 --- a/Documentation/Physics2D.md +++ b/Documentation/Physics2D.md @@ -242,7 +242,7 @@ platforms follow their velocity instead of receiving gravity. ### Runnable coverage -`Demos/Physics2D` contains ray/event, geometry/query, nine-mechanism and character +`Examples/Technical/Physics2D` contains ray/event, geometry/query, nine-mechanism and character playgrounds. Its `--validate` mode runs the same scene setup deterministically; `script/build_and_run.sh --verify` stages a native macOS app, exercises controls, checks live state/re-entry and saves real Metal GPU frames and verification JSON. diff --git a/Documentation/Physics2DAdvanced.md b/Documentation/Physics2DAdvanced.md index 30ff8efad..375da51fb 100644 --- a/Documentation/Physics2DAdvanced.md +++ b/Documentation/Physics2DAdvanced.md @@ -127,9 +127,9 @@ Both character modes, exact global revolute anchors, native debug geometry and o ## Runnable acceptance -`Demos/Physics2D` includes an Advanced tab with conveyors/rolling/mixing, wind/drag/damping, axis and sleep controls, a bullet crossing a thin kinematic target, contact point markers and solver presets. The controls use only public runtime APIs. +`Examples/Technical/Physics2D` includes an Advanced tab with conveyors/rolling/mixing, wind/drag/damping, axis and sleep controls, a bullet crossing a thin kinematic target, contact point markers and solver presets. The controls use only public runtime APIs. -From the engine worktree, run `Demos/Physics2D/script/build_and_run.sh --verify` to build/stage/open the native app and generate actual scene GPU frames plus `dist/proof/verification.json`. The executable's `--validate` mode runs deterministic simulation checks without opening a window. Generated bundles and proof files are ignored. +From an AdaExamples checkout, run `Examples/Technical/Physics2D/script/build_and_run.sh --verify` to build/stage/open the native app and generate actual scene GPU frames plus `dist/proof/verification.json`. The executable's `--validate` mode runs deterministic simulation checks without opening a window. Generated bundles and proof files are ignored. The Large worlds tab simulates at global coordinates near 1e9. Its one-millimeter target nudge changes an actual precise ray hit; shifting the render origin keeps the camera view, contact identity and exact hinge intact. Movement/jump and mode controls exercise both virtual and dynamic characters there. Native proof includes `large-world-before.png`, `large-world-shifted.png` and `large-world-dynamic.png`, plus scene re-entry and handle-isolation checks. diff --git a/Documentation/Spine2D.md b/Documentation/Spine2D.md index 51aed45f9..65310337b 100644 --- a/Documentation/Spine2D.md +++ b/Documentation/Spine2D.md @@ -91,11 +91,11 @@ separate render smoke test. ## Visual preview -[SpineNative](../Demos/SpineNative/README.md) runs the production render systems +[SpineNative](https://github.com/AdaEngine/AdaExamples/tree/main/Examples/Technical/SpineNative/README.md) runs the production render systems in a native engine window. Its AdaMCP endpoint exposes exact pose selection and the engine's GPU screenshot tools. -[SpinePreview](../Demos/SpinePreview/README.md) exports the official Spineboy's +[SpinePreview](https://github.com/AdaEngine/AdaExamples/tree/main/Examples/Technical/SpinePreview/README.md) exports the official Spineboy's walk, run, jump and idle poses through the production Swift player. Its portable browser viewer displays the textured triangles, bone overlay and frame scrubber. This preview makes animation behavior visible without requiring a native GPU backend. diff --git a/Documentation/SpriteInstancing.md b/Documentation/SpriteInstancing.md index 2fdabf106..ab63b3962 100644 --- a/Documentation/SpriteInstancing.md +++ b/Documentation/SpriteInstancing.md @@ -81,7 +81,7 @@ paths, including alpha picking, padded texture readback and tile occlusion. The developer command also verifies host-tool discovery, real fresh GLSL → SPIR-V → Tint WGSL compilation and WGSL binding reflection: six tests in four suites pass. -The separate `Demos/SpriteInstancingValidation` app exercises the same production +The separate `Examples/Technical/SpriteInstancingValidation` app exercises the same production SpriteRenderSystem / SpriteDrawPass / shaders on all three validated runtimes: | Runtime | Backend | Result | diff --git a/Editor/Sources/AdaEditor/Assets/Documentation/catalog.json b/Editor/Sources/AdaEditor/Assets/Documentation/catalog.json index b86f6aa8d..53c4261bb 100644 --- a/Editor/Sources/AdaEditor/Assets/Documentation/catalog.json +++ b/Editor/Sources/AdaEditor/Assets/Documentation/catalog.json @@ -3115,13 +3115,18 @@ }, { "kind": "text", - "text": "Run the included animated sprite and audio example from the engine root:", + "text": "Clone AdaExamples and run its animated sprite and audio example:", "level": 0, - "links": [] + "links": [ + { + "title": "AdaExamples", + "destination": "https://github.com/AdaEngine/AdaExamples" + } + ] }, { "kind": "code", - "text": "swift run MovieMakerDemo --write-movie Movies/demo.png --fixed-fps 60 --resolution 1280x720 --quit-after 180", + "text": "swift run --package-path Examples/Technical/MovieMaker MovieMakerDemo --write-movie Movies/demo.png --fixed-fps 60 --resolution 1280x720 --quit-after 180", "language": "sh", "links": [] }, diff --git a/Editor/scripts/stage-build-sdk.py b/Editor/scripts/stage-build-sdk.py index c2cba1b41..71963c196 100644 --- a/Editor/scripts/stage-build-sdk.py +++ b/Editor/scripts/stage-build-sdk.py @@ -45,7 +45,7 @@ def stage(engine, compiler, destination, swan=None): staged = Path(temporary) / "BuildSDK" staged.mkdir() copy_inputs(engine, staged / "AdaEngine", ["Package.swift", "Package.resolved", ".swiftlint.yml", "LICENSE", "NOTICE", - "Sources", "Plugins", "Tools", "Tests", "Demos", "Assets", "script"]) + "Sources", "Plugins", "Tools", "Tests", "Assets", "script"]) copy_inputs(compiler, staged / "AdaScript", ["Package.swift", "Package.resolved", "LICENSE", "Makefile", "src", "binding", "tools", "Tests", "examples", "gravity", "gravity.exe"]) host = staged / "AdaScript" / ("gravity.exe" if os.name == "nt" else "gravity") diff --git a/Editor/scripts/test_stage_build_sdk.py b/Editor/scripts/test_stage_build_sdk.py index 3f2105cc5..c847432b9 100644 --- a/Editor/scripts/test_stage_build_sdk.py +++ b/Editor/scripts/test_stage_build_sdk.py @@ -44,7 +44,7 @@ def test_source_sdk_is_self_contained_and_omits_private_build_state(self): root = Path(temporary) engine = root / "engine" compiler = root / "compiler" - for base, files in [(engine, ["Package.swift", "LICENSE", "Sources/Runtime.swift", "Sources/.build/private", ".git/config", "script/ensure_tint.py"]), + for base, files in [(engine, ["Package.swift", "LICENSE", "Sources/Runtime.swift", "Sources/.build/private", ".git/config", "script/ensure_tint.py", "Demos/Example/binary.glb"]), (compiler, ["Package.swift", "LICENSE", "src/shared/header.h", "tools/aot_build.py", "binding/Runtime.swift"])]: for name in files: path = base / name @@ -61,6 +61,7 @@ def test_source_sdk_is_self_contained_and_omits_private_build_state(self): self.assertTrue((destination / "AdaScript/gravity").is_file()) self.assertTrue((destination / "AdaScript/LICENSE").is_file()) self.assertFalse((destination / "AdaEngine/.git").exists()) + self.assertFalse((destination / "AdaEngine/Demos").exists()) self.assertFalse((destination / "AdaEngine/Sources/.build").exists()) self.assertFalse((engine / "sdk.json").exists()) # A second stage replaces the complete SDK without accumulating nested copies. @@ -69,15 +70,24 @@ def test_source_sdk_is_self_contained_and_omits_private_build_state(self): def test_android_sdk_keeps_dawn_static_library_after_relocation(self): with tempfile.TemporaryDirectory() as temporary: - root=Path(temporary); engine=root/"engine"; compiler=root/"compiler"; swan=root/"swan" + root=Path(temporary) + engine=root/"engine" + compiler=root/"compiler" + swan=root/"swan" for base, names in [(engine,["Package.swift","Tools/Android/android.py"]),(compiler,["Package.swift","tools/aot_build.py"]), (swan,["Package.swift","Sources/WebGPU/WebGPU.swift","Dawn/dist/android.artifactbundle/info.json","Dawn/dist/android.artifactbundle/arm64/libwebgpu_dawn.a"])]: for name in names: - path=base/name;path.parent.mkdir(parents=True,exist_ok=True);path.write_text("fixture") - host=compiler/"gravity";host.write_text("#!/bin/sh\nexit 0\n");host.chmod(0o755) + path=base/name + path.parent.mkdir(parents=True,exist_ok=True) + path.write_text("fixture") + host=compiler/"gravity" + host.write_text("#!/bin/sh\nexit 0\n") + host.chmod(0o755) destination=root/"Studio.app/Contents/Resources/BuildSDK" sdk.stage(engine,compiler,destination,swan) - moved=root/"Relocated/BuildSDK";moved.parent.mkdir();destination.rename(moved) + moved=root/"Relocated/BuildSDK" + moved.parent.mkdir() + destination.rename(moved) self.assertTrue(json.loads((moved/"sdk.json").read_text())["android"]) self.assertEqual((moved/"Swan/Dawn/dist/android.artifactbundle/arm64/libwebgpu_dawn.a").read_text(),"fixture") self.assertTrue((moved/"AdaEngine/Tools/Android/android.py").is_file()) diff --git a/Package.swift b/Package.swift index 49c3d26e9..9eddb864e 100644 --- a/Package.swift +++ b/Package.swift @@ -70,12 +70,13 @@ extension String { let applePlatforms: [Platform] = [.iOS, .macOS, .tvOS, .watchOS, .visionOS] var products: [Product] = [ - .executable(name: "MovieMakerDemo", targets: ["MovieMakerDemo"]), - .executable(name: "NavigationDemo", targets: ["NavigationDemo"]), - .executable(name: "Physics2DDemo", targets: ["Physics2DDemo"]), + // Public runtime modules used by independently packaged examples. + .library(name: "AdaAssets", targets: ["AdaAssets"]), + .library(name: "AdaPlatform", targets: ["AdaPlatform"]), + .library(name: "AdaInput", targets: ["AdaInput"]), + .library(name: "AdaSpineRuntime", targets: ["AdaSpineRuntime"]), .library(name: "AdaNavigation", targets: ["AdaNavigation"]), .library(name: "AdaNavigationBaking", targets: ["AdaNavigationBaking"]), - .executable(name: "SpinePreview", targets: ["SpinePreview"]), .library(name: "AdaSpine", targets: ["AdaSpine"]), .library(name: "CAndroid", targets: ["CAndroid"]), .executable(name: "AdaWebPlayer", targets: ["AdaWebPlayer"]), @@ -83,7 +84,6 @@ var products: [Product] = [ .executable(name: "AdaScriptTest", targets: ["AdaScriptTest"]), .library(name: "AdaUIDescription", targets: ["AdaUIDescription"]), .library(name: "AdaA2UI", targets: ["AdaA2UI"]), - .executable(name: "A2UIFormDemo", targets: ["A2UIFormDemo"]), .library( name: "AdaEngine", targets: ["AdaEngine"] @@ -172,7 +172,6 @@ var products: [Product] = [ ]) ] -products.append(.library(name: "AndroidDemo", type: .dynamic, targets: ["AndroidDemo"])) // MARK: - Targets @@ -344,26 +343,6 @@ let adaEngineMacros: Target = .macro( // MARK: Other Targets var targets: [Target] = [ - .executableTarget( - name: "MovieMakerDemo", - dependencies: ["AdaEngine"], - path: "Demos/MovieMaker", - swiftSettings: swiftSettings - ), - .executableTarget( - name: "Physics2DDemo", - dependencies: ["AdaEngine"], - path: "Demos/Physics2D", - exclude: ["README.md", "script", "dist", ".codex"], - swiftSettings: swiftSettings - ), - .executableTarget( - name: "NavigationDemo", - dependencies: ["AdaEngine", "AdaNavigationBaking"], - path: "Demos/Navigation", - resources: [.copy("NavigationRobot.ada")], - swiftSettings: swiftSettings - ), .target( name: "Detour", path: "Sources/RecastNavigation/Detour", @@ -381,14 +360,6 @@ var targets: [Target] = [ .target(name: "AdaNavigation", dependencies: ["CNavigation", "Math"], swiftSettings: swiftSettings), .target(name: "AdaNavigationBaking", dependencies: ["AdaNavigation", "CNavigationBaking", "Math"], swiftSettings: swiftSettings), .testTarget(name: "AdaNavigationTests", dependencies: ["AdaNavigation", "AdaNavigationBaking", "Math"], swiftSettings: swiftSettings), - .executableTarget( - name: "SpinePreview", - dependencies: ["AdaSpineRuntime"], - path: "Demos/SpinePreview", - exclude: ["README.md"], - resources: [.copy("Assets")], - swiftSettings: swiftSettings - ), .target( name: "SpineRuntimeC", exclude: ["LICENSE", "UPSTREAM.md"], @@ -411,14 +382,6 @@ var targets: [Target] = [ .linkedLibrary("log", .when(platforms: [.android])), ] ), - .target( - name: "AndroidDemo", - dependencies: ["AdaEngine", "CAndroid"], - path: "Demos/AndroidDemo", - exclude: ["README.md"], - resources: [.copy("Assets")], - swiftSettings: swiftSettings - ), adaEngineTarget, adaEngineEmbeddable, adaEngineMacros, @@ -647,17 +610,6 @@ var targets: [Target] = [ resources: [.process("Resources")], swiftSettings: swiftSettings ), - .executableTarget( - name: "A2UIFormDemo", - dependencies: ["AdaA2UI", "AdaEngine", "AdaPlatform"], - path: "Demos/A2UIForm", - exclude: ["README.md", "script"] + ( - FileManager.default.fileExists(atPath: URL(fileURLWithPath: #filePath).deletingLastPathComponent().appendingPathComponent("Demos/A2UIForm/dist").path) - ? ["dist"] : [] - ), - resources: [.copy("Fixtures")], - swiftSettings: swiftSettings - ), .adaTarget( name: "AdaUI", dependencies: [ @@ -942,9 +894,7 @@ if isWGPUEnabled { // MARK: Extra #if os(Linux) -products.append(.executable(name: "LinuxSmoke", targets: ["LinuxSmoke"])) targets += [ - .executableTarget(name: "LinuxSmoke", dependencies: ["AdaEngine", "AdaPlatform"], path: "Demos/LinuxSmoke", swiftSettings: swiftSettings), .testTarget(name: "AdaPlatformTests", dependencies: ["AdaPlatform", "AdaECS", "AdaInput", "AdaRender", "AdaUI", "AdaUtils", "Math", "X11"], swiftSettings: swiftSettings), ] #endif @@ -1438,7 +1388,7 @@ targets += [ .testTarget( name: "AdaTransformTests", dependencies: [ - "AdaECS", + "AdaECS", "AdaTransform", "Math" ], @@ -1523,17 +1473,6 @@ targets += [ //targets.append(contentsOf: swiftLintTargets) #endif -// Portable GPU readback QA app; kept outside the engine and Studio runtime. -products.append(.executable(name: "SpriteInstancingValidation", targets: ["SpriteInstancingValidation"])) -targets.append(.executableTarget( - name: "SpriteInstancingValidation", - dependencies: ["AdaApp", "AdaAssets", "AdaCorePipelines", "AdaECS", "AdaInput", "AdaPlatform", "AdaRender", "AdaSprite", "AdaText", "AdaTransform", "AdaUI", "AdaUtils", "Math"], - path: "Demos/SpriteInstancingValidation", - exclude: ["project.yml", "README.md", "SpriteInstancingValidation.xcodeproj"], - swiftSettings: swiftSettings, - linkerSettings: wasmExecutableLinkerSettings -)) - if let config = dlssBuildConfiguration, let includes = config["includePaths"] as? [String], let libraries = config["libraries"] as? [String] { let includeFlags = includes.flatMap { ["-I", $0] } diff --git a/Sources/AdaEngine/AdaEngine.docc/MovieMaker.md b/Sources/AdaEngine/AdaEngine.docc/MovieMaker.md index b581bf8b6..9e029874d 100644 --- a/Sources/AdaEngine/AdaEngine.docc/MovieMaker.md +++ b/Sources/AdaEngine/AdaEngine.docc/MovieMaker.md @@ -103,10 +103,10 @@ It returns `nil` when `--write-movie` is absent, so a game's entry point can cho between its normal launch and a recording. Parsing alone does not change app startup; pass the configuration to `MovieMaker.record` with your game's setup. -Run the included animated sprite and audio example from the engine root: +Clone [AdaExamples](https://github.com/AdaEngine/AdaExamples) and run its animated sprite and audio example: ```sh -swift run MovieMakerDemo --write-movie Movies/demo.png --fixed-fps 60 --resolution 1280x720 --quit-after 180 +swift run --package-path Examples/Technical/MovieMaker MovieMakerDemo --write-movie Movies/demo.png --fixed-fps 60 --resolution 1280x720 --quit-after 180 ``` Additional flags are `--movie-warmup N` and `--movie-no-audio`. `--quit-after N` diff --git a/Sources/AdaNavigation/AdaNavigation.docc/AdaNavigation.md b/Sources/AdaNavigation/AdaNavigation.docc/AdaNavigation.md index 419a298b7..b0b8aa928 100644 --- a/Sources/AdaNavigation/AdaNavigation.docc/AdaNavigation.md +++ b/Sources/AdaNavigation/AdaNavigation.docc/AdaNavigation.md @@ -149,7 +149,9 @@ controller on the character as usual. The host still owns mesh baking, loading a ## Playground demo -Run `swift run NavigationDemo` for a 3D playground with a robot and obstacles. +Set `ADAEXAMPLES_PATH` to a clone of [AdaExamples](https://github.com/AdaEngine/AdaExamples). + +Run `swift run --package-path "$ADAEXAMPLES_PATH/Examples/Technical/Navigation" NavigationDemo` for a 3D playground with a robot and obstacles. On Windows, use `./script/run_navigation_demo_windows.ps1`; it selects the checked-in AdaScript runtime and prepares the shader compiler automatically. Click the floor to choose a destination. Buttons cycle goals, toggle the mesh, @@ -161,7 +163,7 @@ floor switches to manual goals; the **Патруль AdaScript** button resumes `NavigationDemo --validate` runs the same fixture, file round trip, automatic requests and physics movement without creating a renderer. To build without GPU dependencies on Windows, use `$env:ADAENGINE_HEADLESS='1'` before -`swift run NavigationDemo --validate`. Clear that setting for the interactive demo. +`swift run --package-path "$ADAEXAMPLES_PATH/Examples/Technical/Navigation" NavigationDemo --validate`. Clear that setting for the interactive demo. `NavigationDemo --capture /absolute/path/playground.png` renders offscreen using the real GPU backend, checks viewport mouse picking, and saves the resulting frame. diff --git a/Tools/Android/README.md b/Tools/Android/README.md index 6c8a623e8..d60fa7c17 100644 --- a/Tools/Android/README.md +++ b/Tools/Android/README.md @@ -1,5 +1,7 @@ # Native Android apps +The Android demo is maintained in [AdaExamples](https://github.com/AdaEngine/AdaExamples). Set `ADAEXAMPLES_PATH` to that checkout when using the commands below. + AdaEngine can build a native Swift shared library, package it in a debug-signed APK, install it, and launch it on an authorized device or emulator. The host uses Android NativeActivity, native WebGPU (Swan + Dawn/Vulkan), Choreographer, and Swift Concurrency. @@ -64,16 +66,16 @@ From the AdaEngine repository root: ```sh python3 Tools/Android/android.py doctor python3 Tools/Android/android.py devices -python3 Tools/Android/android.py build -python3 Tools/Android/android.py run --serial emulator-5554 +python3 Tools/Android/android.py build --package "$ADAEXAMPLES_PATH/Examples/Technical/AndroidDemo" +python3 Tools/Android/android.py run --package "$ADAEXAMPLES_PATH/Examples/Technical/AndroidDemo" --serial emulator-5554 # Convenience wrapper; optionally reads task-local .build-android/local-env.sh -Tools/Android/run_demo.sh --serial emulator-5554 +Tools/Android/run_demo.sh --package "$ADAEXAMPLES_PATH/Examples/Technical/AndroidDemo" --serial emulator-5554 ``` `run` builds, installs, and starts the app. To reuse an APK: ```sh -python3 Tools/Android/android.py run \ +python3 Tools/Android/android.py run --package "$ADAEXAMPLES_PATH/Examples/Technical/AndroidDemo" \ --apk .build-android/apk/AndroidDemo-debug.apk --serial emulator-5554 ``` @@ -126,7 +128,7 @@ python3 /path/to/AdaEngine/Tools/Android/android.py run \ For the app target's own SwiftPM resources, pass `AndroidResourceBundle.bundle(named: "PackageName_TargetName")` as `assetBundle` -to `WindowGroup` or `DefaultAppWindow` on Android. See `Demos/AndroidDemo`. +to `WindowGroup` or `DefaultAppWindow` on Android. See [AndroidDemo](https://github.com/AdaEngine/AdaExamples/tree/main/Examples/Technical/AndroidDemo). The packaging tool extracts bundles from the APK into app-private storage before engine bootstrap; engine shaders/fonts use the same path. Writable data and caches also stay inside the app's private files directory. diff --git a/Tools/Android/android.py b/Tools/Android/android.py index ec8c49c92..c0a57391a 100644 --- a/Tools/Android/android.py +++ b/Tools/Android/android.py @@ -154,7 +154,8 @@ def compile_java(args, output, sdk, build_tools, env): dex=output/"dex" # Both directories are task-owned, under the selected APK output directory. for directory in (classes, dex): - if directory.exists(): shutil.rmtree(directory) + if directory.exists(): + shutil.rmtree(directory) directory.mkdir(parents=True) javac=Path(env["JAVA_HOME"])/"bin/javac" if env.get("JAVA_HOME") else Path(shutil.which("javac") or "javac") android_jar=tool(sdk/"platforms"/f"android-{args.target_sdk}"/"android.jar") @@ -322,7 +323,7 @@ def launch(args): def parser(): parser=argparse.ArgumentParser(description=__doc__) parser.add_argument("command",choices=["doctor","build","run","devices"]) - parser.add_argument("--package",default=str(ROOT)) + parser.add_argument("--package", help="Swift package to build; AndroidDemo lives in AdaExamples/Examples/Technical/AndroidDemo") parser.add_argument("--product",default="AndroidDemo") parser.add_argument("--application-id",default="org.adaengine.android.demo") parser.add_argument("--label",default="AdaEngine Android") @@ -348,11 +349,17 @@ def parser(): return parser def main(): - args=parser().parse_args() + cli=parser() + args=cli.parse_args() + if args.command in ("build", "run") and not args.apk and not args.package: + cli.error("--package is required to build; pass the AndroidDemo package in AdaExamples/Examples/Technical/AndroidDemo") try: - if args.command=="build": build(args) - elif args.command=="run": launch(args) - elif args.command=="devices": run([tool(sdk_path(args)/"platform-tools/adb"),"devices","-l"]) + if args.command=="build": + build(args) + elif args.command=="run": + launch(args) + elif args.command=="devices": + run([tool(sdk_path(args)/"platform-tools/adb"),"devices","-l"]) else: validate_options(args) env=build_env() @@ -361,7 +368,8 @@ def main(): print("Swift SDK:",swift_sdk_root(args)[1]) print("NDK:",ndk_host(ndk_path(args))) sdk=sdk_path(args) - for name in ["aapt2","zipalign","apksigner"]: print("Tool:",tool(sdk/"build-tools"/args.build_tools/name)) + for name in ["aapt2","zipalign","apksigner"]: + print("Tool:",tool(sdk/"build-tools"/args.build_tools/name)) print("Android platform:",tool(sdk/"platforms"/f"android-{args.target_sdk}"/"android.jar")) run([tool(sdk/"platform-tools/adb"),"devices","-l"]) except (AndroidError,OSError) as error: diff --git a/Tools/Android/tests/test_android.py b/Tools/Android/tests/test_android.py index 8f0fd1c75..77fc0b27a 100644 --- a/Tools/Android/tests/test_android.py +++ b/Tools/Android/tests/test_android.py @@ -1,5 +1,7 @@ import importlib.util from pathlib import Path +import subprocess +import sys import tempfile import unittest import xml.etree.ElementTree as ET @@ -10,17 +12,28 @@ spec.loader.exec_module(android) class AndroidPackagingTests(unittest.TestCase): + def test_build_requires_explicit_external_package(self): + for command in ("build", "run"): + result = subprocess.run([sys.executable, str(Path(android.__file__)), command], capture_output=True, text=True) + self.assertEqual(result.returncode, 2) + self.assertIn("--package is required", result.stderr) + def test_apk_preserves_every_manifest_asset_and_utf8_filename(self): with tempfile.TemporaryDirectory() as temporary: - root=Path(temporary); binary=root/"bin"; staging=root/"staging" - bundle=binary/"Game.resources"/"Assets"; bundle.mkdir(parents=True) + root=Path(temporary) + binary=root/"bin" + staging=root/"staging" + bundle=binary/"Game.resources"/"Assets" + bundle.mkdir(parents=True) (bundle/".gitkeep").touch() (bundle/"Текстура 🌈.png").write_bytes(bytes(range(256))) android.stage_resources(binary,staging/"assets") - library=staging/"lib/arm64-v8a/libGame.so"; library.parent.mkdir(parents=True) + library=staging/"lib/arm64-v8a/libGame.so" + library.parent.mkdir(parents=True) library.write_bytes(b"native runtime") apk=root/"unsigned.apk" - with zipfile.ZipFile(apk,"w") as archive: archive.writestr("AndroidManifest.xml",b"manifest") + with zipfile.ZipFile(apk,"w") as archive: + archive.writestr("AndroidManifest.xml",b"manifest") android.append_payload(apk,staging) with zipfile.ZipFile(apk) as archive: self.assertIsNone(archive.testzip()) @@ -33,12 +46,16 @@ def test_apk_preserves_every_manifest_asset_and_utf8_filename(self): def test_java_host_dex_and_utf8_resources_share_the_apk(self): with tempfile.TemporaryDirectory() as temporary: - root=Path(temporary); staging=root/"staging" + root=Path(temporary) + staging=root/"staging" asset=staging/"assets/bundles/Studio.resources/Привет.txt" - asset.parent.mkdir(parents=True); asset.write_bytes("🙂".encode("utf-8")) - dex=root/"classes.dex"; dex.write_bytes(b"dex\n035\0host") + asset.parent.mkdir(parents=True) + asset.write_bytes("🙂".encode("utf-8")) + dex=root/"classes.dex" + dex.write_bytes(b"dex\n035\0host") apk=root/"unsigned.apk" - with zipfile.ZipFile(apk,"w") as archive: archive.writestr("AndroidManifest.xml",b"manifest") + with zipfile.ZipFile(apk,"w") as archive: + archive.writestr("AndroidManifest.xml",b"manifest") android.append_payload(apk,staging,[dex]) with zipfile.ZipFile(apk) as archive: self.assertIsNone(archive.testzip()) @@ -47,8 +64,11 @@ def test_java_host_dex_and_utf8_resources_share_the_apk(self): def test_preserves_nested_resource_manifest_and_binary_font(self): with tempfile.TemporaryDirectory() as temporary: - root=Path(temporary); binary=root/"bin"; assets=root/"assets" - bundle=binary/"Game_GameNative.resources"/"Assets"; bundle.mkdir(parents=True) + root=Path(temporary) + binary=root/"bin" + assets=root/"assets" + bundle=binary/"Game_GameNative.resources"/"Assets" + bundle.mkdir(parents=True) (bundle/"manifest.txt").write_text("game manifest") (bundle/"font.ttf").write_bytes(bytes(range(256))) android.stage_resources(binary,assets) @@ -59,25 +79,33 @@ def test_preserves_nested_resource_manifest_and_binary_font(self): def test_second_abi_does_not_leave_files_deleted_from_bundle(self): with tempfile.TemporaryDirectory() as temporary: - root=Path(temporary); binary=root/"bin"; assets=root/"assets" - bundle=binary/"Game_GameNative.resources"; bundle.mkdir(parents=True) + root=Path(temporary) + binary=root/"bin" + assets=root/"assets" + bundle=binary/"Game_GameNative.resources" + bundle.mkdir(parents=True) (bundle/"old.txt").write_text("old") android.stage_resources(binary,assets) - (bundle/"old.txt").unlink(); (bundle/"new.txt").write_text("new") + (bundle/"old.txt").unlink() + (bundle/"new.txt").write_text("new") android.stage_resources(binary,assets) self.assertFalse((assets/"bundles/Game_GameNative.resources/old.txt").exists()) self.assertEqual((assets/"bundles/manifest.txt").read_text(),"Game_GameNative.resources/new.txt\n") def test_refuses_apk_without_resources(self): with tempfile.TemporaryDirectory() as temporary: - root=Path(temporary); (root/"bin").mkdir() - with self.assertRaises(android.AndroidError): android.stage_resources(root/"bin",root/"assets") + root=Path(temporary) + (root/"bin").mkdir() + with self.assertRaises(android.AndroidError): + android.stage_resources(root/"bin",root/"assets") def test_native_activity_manifest_has_exported_entry_and_no_dex_requirement(self): with tempfile.TemporaryDirectory() as temporary: args=android.parser().parse_args(["build","--label",'Game & "test"']) - path=Path(temporary)/"AndroidManifest.xml"; android.write_manifest(path,args) - manifest=ET.parse(path).getroot(); app=manifest.find("application") + path=Path(temporary)/"AndroidManifest.xml" + android.write_manifest(path,args) + manifest=ET.parse(path).getroot() + app=manifest.find("application") self.assertEqual(app.attrib[android.ANDROID+"hasCode"],"false") self.assertEqual(app.attrib[android.ANDROID+"label"],'Game & "test"') activity=app.find("activity") @@ -86,7 +114,8 @@ def test_native_activity_manifest_has_exported_entry_and_no_dex_requirement(self def test_rejects_incompatible_api_level_before_building(self): args=android.parser().parse_args(["build","--min-sdk","28"]) - with self.assertRaises(android.AndroidError): android.validate_options(args) + with self.assertRaises(android.AndroidError): + android.validate_options(args) def test_studio_manifest_enables_dex_and_excludes_keystore_backup(self): with tempfile.TemporaryDirectory() as temporary: @@ -117,13 +146,15 @@ def test_custom_activity_requires_qualified_class_name(self): def test_browser_callback_returns_to_the_existing_activity(self): with tempfile.TemporaryDirectory() as temporary: - source=Path(temporary)/"StudioActivity.java"; source.write_text("class StudioActivity {}") + source=Path(temporary)/"StudioActivity.java" + source.write_text("class StudioActivity {}") args=android.parser().parse_args([ "build", "--activity", "org.adaengine.studio.StudioActivity", "--java-source", str(source), "--callback-url", "adaeditor://cloud/callback" ]) android.validate_options(args) - path=Path(temporary)/"AndroidManifest.xml"; android.write_manifest(path,args) + path=Path(temporary)/"AndroidManifest.xml" + android.write_manifest(path,args) activity=ET.parse(path).getroot().find("application/activity") self.assertEqual(activity.attrib[android.ANDROID+"launchMode"],"singleTask") link=activity.findall("intent-filter")[1] @@ -137,7 +168,8 @@ def test_callback_without_a_java_host_is_rejected(self): with self.assertRaisesRegex(android.AndroidError,"Java host"): android.validate_options(args) -if __name__=="__main__": unittest.main() +if __name__=="__main__": + unittest.main() class AndroidWebGPUSetupTests(unittest.TestCase): def test_requires_explicit_android_dawn_setup(self): @@ -148,8 +180,10 @@ def test_requires_explicit_android_dawn_setup(self): def test_rejects_an_unbuilt_abi_before_swift_build(self): import json with tempfile.TemporaryDirectory() as temporary: - root=Path(temporary); (root/"Package.swift").write_text("// fixture") - bundle=root/"android.artifactbundle"; bundle.mkdir() + root=Path(temporary) + (root/"Package.swift").write_text("// fixture") + bundle=root/"android.artifactbundle" + bundle.mkdir() (bundle/"lib.a").write_bytes(b"library") (bundle/"info.json").write_text(json.dumps({"artifacts":{"dawn":{"variants":[ {"path":"lib.a","supportedTriples":["aarch64-unknown-linux-android29"]} @@ -164,7 +198,9 @@ def test_rejects_an_unbuilt_abi_before_swift_build(self): class AndroidSDKSelectionTests(unittest.TestCase): def test_export_staging_does_not_change_sdk_import_paths(self): with tempfile.TemporaryDirectory() as temporary: - root=Path(temporary); bundle=root/"sdk.artifactbundle/swift-android"; bundle.mkdir(parents=True) + root=Path(temporary) + bundle=root/"sdk.artifactbundle/swift-android" + bundle.mkdir(parents=True) first=android.parser().parse_args(["build","--scratch",str(root/"cache"),"--output",str(root/"stage-one")]) second=android.parser().parse_args(["build","--scratch",str(root/"cache"),"--output",str(root/"stage-two")]) self.assertEqual(android.select_swift_sdks(first,bundle),android.select_swift_sdks(second,bundle)) diff --git a/Tools/Tint/README.md b/Tools/Tint/README.md index 65b2c233e..0bd7b612f 100644 --- a/Tools/Tint/README.md +++ b/Tools/Tint/README.md @@ -45,4 +45,4 @@ GPU validation build cache. For direct WASM builds set both both for its nested build automatically. Browser/mobile runtime checks use the separate -[SpriteInstancingValidation](../../Demos/SpriteInstancingValidation/README.md) app. +[SpriteInstancingValidation](https://github.com/AdaEngine/AdaExamples/tree/main/Examples/Technical/SpriteInstancingValidation/README.md) app. diff --git a/script/build_and_run.sh b/script/build_and_run.sh index b1603620e..18550ab37 100755 --- a/script/build_and_run.sh +++ b/script/build_and_run.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash set -euo pipefail -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -exec "$REPO_ROOT/Demos/A2UIForm/script/build_and_run.sh" "$@" +: "${ADAEXAMPLES_PATH:?Set ADAEXAMPLES_PATH to an AdaExamples checkout}" +exec "$ADAEXAMPLES_PATH/Examples/Technical/A2UIForm/script/build_and_run.sh" "$@" diff --git a/script/run_navigation_demo_windows.ps1 b/script/run_navigation_demo_windows.ps1 index 4d504f090..553033581 100644 --- a/script/run_navigation_demo_windows.ps1 +++ b/script/run_navigation_demo_windows.ps1 @@ -1,40 +1,14 @@ -# Run the navigation playground with the same Windows AdaScript dependency used by Studio. +# Forward to the independently packaged navigation example. [CmdletBinding()] -param( - [switch]$ValidateScript, - [string]$CapturePath, - [string]$ScratchPath -) - +param([switch]$ValidateScript, [string]$CapturePath, [string]$ScratchPath, [string]$ExamplesRoot = $env:ADAEXAMPLES_PATH) $ErrorActionPreference = 'Stop' -$demoRoot = Split-Path -Parent $PSScriptRoot -$demoEnvironment = @{} -$demoVariables = @('ADAENGINE_HEADLESS', 'ADAENGINE_DISABLE_SWAN', 'ADAENGINE_GRAVITY_PACKAGE_PATH', 'TINT_EXECUTABLE') -foreach ($name in $demoVariables) { $demoEnvironment[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') } +if (-not $ExamplesRoot) { throw 'Set ADAEXAMPLES_PATH or pass -ExamplesRoot for the AdaExamples checkout.' } +$launcher = Join-Path $ExamplesRoot 'Examples/Technical/Navigation/script/run_windows.ps1' +if (-not (Test-Path -LiteralPath $launcher -PathType Leaf)) { throw "Navigation example not found: $launcher" } +$previousEngineRoot = $env:ADAENGINE_ROOT try { - if (-not $env:ADAENGINE_GRAVITY_PACKAGE_PATH) { - $demoGravity = & python (Join-Path $PSScriptRoot 'ensure_windows_gravity.py') - if ($LASTEXITCODE -ne 0) { throw 'Windows AdaScript dependency preparation failed.' } - $env:ADAENGINE_GRAVITY_PACKAGE_PATH = ($demoGravity | Select-Object -Last 1).Trim() - } - $env:ADAENGINE_HEADLESS = if ($ValidateScript) { '1' } else { '0' } - if (-not $ValidateScript) { - $env:ADAENGINE_DISABLE_SWAN = '0' - if (-not $env:TINT_EXECUTABLE) { - $demoTint = & python (Join-Path $PSScriptRoot 'ensure_tint.py') --jobs 2 - if ($LASTEXITCODE -ne 0) { throw 'Tint preparation failed.' } - $env:TINT_EXECUTABLE = ($demoTint | Select-Object -Last 1).Trim() - } - } - $demoArguments = @('run', '--package-path', $demoRoot) - if ($ScratchPath) { $demoArguments += @('--scratch-path', $ScratchPath) } - $demoArguments += 'NavigationDemo' - if ($ValidateScript) { $demoArguments += '--validate-script' } - elseif ($CapturePath) { - $demoArguments += @('--capture', $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($CapturePath)) - } - & swift @demoArguments - if ($LASTEXITCODE -ne 0) { throw "Navigation demo failed with exit code $LASTEXITCODE." } + $env:ADAENGINE_ROOT = Split-Path -Parent $PSScriptRoot + & $launcher -ValidateScript:$ValidateScript -CapturePath $CapturePath -ScratchPath $ScratchPath } finally { - foreach ($name in $demoVariables) { [Environment]::SetEnvironmentVariable($name, $demoEnvironment[$name], 'Process') } + $env:ADAENGINE_ROOT = $previousEngineRoot } diff --git a/script/webgpu_validation_package.py b/script/webgpu_validation_package.py index 5ef52175f..2e6da0c50 100644 --- a/script/webgpu_validation_package.py +++ b/script/webgpu_validation_package.py @@ -2,6 +2,7 @@ """Stage focused production test sources without building unrelated demo/editor targets.""" import argparse import json +import os from pathlib import Path import shutil @@ -9,7 +10,14 @@ parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("destination", type=Path) parser.add_argument("--include-sponza-demo", action="store_true", help="Build the Sponza demo and camera tests with the same renderer objects") +parser.add_argument("--examples-root", type=Path, default=os.environ.get("ADAEXAMPLES_PATH"), help="AdaExamples checkout for optional Sponza validation") args = parser.parse_args() +if args.include_sponza_demo: + if args.examples_root is None: + parser.error("--include-sponza-demo requires --examples-root or ADAEXAMPLES_PATH pointing to AdaExamples") + sponza_source = args.examples_root.resolve() / "Examples/Technical/Sponza" + if not (sponza_source / "SponzaDemo.swift").is_file(): + parser.error("The examples checkout does not contain Examples/Technical/Sponza") destination = args.destination.resolve() sources = destination / "Tests/RenderingValidationTests" sources.mkdir(parents=True, exist_ok=True) @@ -109,20 +117,21 @@ dependencies = ',\n'.join('.product(name: ' + json.dumps(module) + ', package: "AdaEngine")' for module in modules) extra_targets = "" if args.include_sponza_demo: - demo = destination / "Demos/Sponza" - camera = destination / "Demos/SponzaCamera" + demo = destination / "Examples/Sponza" + camera = destination / "Examples/SponzaCamera" demo.mkdir(parents=True, exist_ok=True) camera.mkdir(parents=True, exist_ok=True) for name in ["SponzaDemo.swift", "SponzaDemoProfiler.swift", "SponzaLighting.swift", "SponzaStutterProfiler.swift", "SponzaWarmup.swift"]: - shutil.copy2(root / "Demos/Sponza" / name, demo / name) - for source in (root / "Demos/Sponza/Camera").glob("*.swift"): + shutil.copy2(sponza_source / name, demo / name) + shutil.copytree(sponza_source / "Assets", demo / "Assets", dirs_exist_ok=True) + for source in (sponza_source / "Camera").glob("*.swift"): shutil.copy2(source, camera / source.name) - shutil.copy2(root / "Demos/Sponza/Tests/SponzaCameraTests.swift", sources / "SponzaCameraTests.swift") + shutil.copy2(sponza_source / "Tests/SponzaCameraTests.swift", sources / "SponzaCameraTests.swift") dependencies += ',\n"SponzaCamera"' extra_targets = ''', .target(name: "SponzaCamera", dependencies: [ - .product(name: "AdaTransform", package: "AdaEngine"), .product(name: "Math", package: "AdaEngine")], path: "Demos/SponzaCamera"), + .product(name: "AdaTransform", package: "AdaEngine"), .product(name: "Math", package: "AdaEngine")], path: "Examples/SponzaCamera"), .executableTarget(name: "SponzaDemo", dependencies: ["SponzaCamera", .product(name: "AdaEngine", package: "AdaEngine"), - .product(name: "Math", package: "AdaEngine")], path: "Demos/Sponza")''' + .product(name: "Math", package: "AdaEngine")], path: "Examples/Sponza", resources: [.copy("Assets")])''' (destination / "Package.swift").write_text('''// swift-tools-version: 6.2 import PackageDescription let package = Package(name: "RenderingValidation", platforms: [.macOS(.v15)], dependencies: [