From 8313656acb7d65c3258a1dad2192969f521131be Mon Sep 17 00:00:00 2001 From: Untold Engine Date: Wed, 6 May 2026 08:55:53 -0700 Subject: [PATCH] [Docs] Moved documentation to use mkdocs --- .github/workflows/docs.yml | 23 + docs/01-Intro.md | 76 - docs/API/GettingStarted.md | 2 +- docs/Contributor/ContributionGuidelines.md | 6 - docs/Contributor/Formatting.md | 8 +- docs/Contributor/versioning.md | 8 +- docs/index.md | 174 + mkdocs.yml | 93 + site/404.html | 1875 +++++ site/API/GettingStarted/index.html | 2571 +++++++ site/API/Optimizations/index.html | 2320 ++++++ site/API/SpatialDebugger/index.html | 2536 +++++++ site/API/UsageExamples/index.html | 2453 ++++++ site/API/UsingAnimationSystem/index.html | 2205 ++++++ site/API/UsingAsyncLoading/index.html | 2425 ++++++ site/API/UsingCameraSystem/index.html | 2313 ++++++ site/API/UsingGaussianSystem/index.html | 2130 ++++++ .../UsingGeometryStreamingSystem/index.html | 2463 ++++++ site/API/UsingInputSystem/index.html | 2195 ++++++ .../UsingLOD-Batching-Streaming/index.html | 2233 ++++++ site/API/UsingLODSystem/index.html | 3273 ++++++++ site/API/UsingLightingSystem/index.html | 2114 ++++++ site/API/UsingMaterials/index.html | 2669 +++++++ site/API/UsingPhysicsSystem/index.html | 2400 ++++++ site/API/UsingPostFX/index.html | 2402 ++++++ site/API/UsingProfiler/index.html | 2219 ++++++ site/API/UsingRegistrationSystem/index.html | 2161 ++++++ site/API/UsingRenderingSystem/index.html | 2277 ++++++ site/API/UsingScenegraph/index.html | 2064 +++++ site/API/UsingSpatialInput/index.html | 2943 ++++++++ site/API/UsingStaticBatchingSystem/index.html | 2365 ++++++ site/API/UsingSteeringSystem/index.html | 2238 ++++++ site/API/UsingTheExporter/index.html | 2228 ++++++ site/API/UsingTheLogger/index.html | 2385 ++++++ site/API/UsingTransformSystem/index.html | 2573 +++++++ site/API/UsingUntoldEngineCLI/index.html | 2385 ++++++ site/Architecture/assetFormat/index.html | 2640 +++++++ site/Architecture/assetProfiler/index.html | 2606 +++++++ .../asset_remote_streaming/index.html | 2965 ++++++++ site/Architecture/batchingSystem/index.html | 2464 ++++++ .../geometryStreamingSystem/index.html | 2898 +++++++ site/Architecture/lodSystem/index.html | 2399 ++++++ .../meshResourceManager/index.html | 2330 ++++++ site/Architecture/outOfCore/index.html | 2391 ++++++ .../progressiveAssetLoader/index.html | 2450 ++++++ site/Architecture/renderingSystem/index.html | 2722 +++++++ .../streamingCacheLifecycle/index.html | 2377 ++++++ .../streamingRegionManager/index.html | 2539 +++++++ .../textureStreamingSystem/index.html | 3189 ++++++++ .../tilebasedstreaming/index.html | 3654 +++++++++ .../Architecture/xrRenderingSystem/index.html | 2591 +++++++ .../ContributionGuidelines/index.html | 2342 ++++++ site/Contributor/Formatting/index.html | 2187 ++++++ site/Contributor/versioning/index.html | 1930 +++++ site/assets/images/favicon.png | Bin 0 -> 1870 bytes .../assets/javascripts/bundle.79ae519e.min.js | 16 + .../javascripts/bundle.79ae519e.min.js.map | 7 + .../javascripts/lunr/min/lunr.ar.min.js | 1 + .../javascripts/lunr/min/lunr.da.min.js | 18 + .../javascripts/lunr/min/lunr.de.min.js | 18 + .../javascripts/lunr/min/lunr.du.min.js | 18 + .../javascripts/lunr/min/lunr.el.min.js | 1 + .../javascripts/lunr/min/lunr.es.min.js | 18 + .../javascripts/lunr/min/lunr.fi.min.js | 18 + .../javascripts/lunr/min/lunr.fr.min.js | 18 + .../javascripts/lunr/min/lunr.he.min.js | 1 + .../javascripts/lunr/min/lunr.hi.min.js | 1 + .../javascripts/lunr/min/lunr.hu.min.js | 18 + .../javascripts/lunr/min/lunr.hy.min.js | 1 + .../javascripts/lunr/min/lunr.it.min.js | 18 + .../javascripts/lunr/min/lunr.ja.min.js | 1 + .../javascripts/lunr/min/lunr.jp.min.js | 1 + .../javascripts/lunr/min/lunr.kn.min.js | 1 + .../javascripts/lunr/min/lunr.ko.min.js | 1 + .../javascripts/lunr/min/lunr.multi.min.js | 1 + .../javascripts/lunr/min/lunr.nl.min.js | 18 + .../javascripts/lunr/min/lunr.no.min.js | 18 + .../javascripts/lunr/min/lunr.pt.min.js | 18 + .../javascripts/lunr/min/lunr.ro.min.js | 18 + .../javascripts/lunr/min/lunr.ru.min.js | 18 + .../javascripts/lunr/min/lunr.sa.min.js | 1 + .../lunr/min/lunr.stemmer.support.min.js | 1 + .../javascripts/lunr/min/lunr.sv.min.js | 18 + .../javascripts/lunr/min/lunr.ta.min.js | 1 + .../javascripts/lunr/min/lunr.te.min.js | 1 + .../javascripts/lunr/min/lunr.th.min.js | 1 + .../javascripts/lunr/min/lunr.tr.min.js | 18 + .../javascripts/lunr/min/lunr.vi.min.js | 1 + .../javascripts/lunr/min/lunr.zh.min.js | 1 + site/assets/javascripts/lunr/tinyseg.js | 206 + site/assets/javascripts/lunr/wordcut.js | 6708 +++++++++++++++++ .../workers/search.2c215733.min.js | 42 + .../workers/search.2c215733.min.js.map | 7 + site/assets/stylesheets/main.484c7ddc.min.css | 1 + .../stylesheets/main.484c7ddc.min.css.map | 1 + .../stylesheets/palette.ab4e12ef.min.css | 1 + .../stylesheets/palette.ab4e12ef.min.css.map | 1 + site/images/editor-highlight-1.png | Bin 0 -> 3260503 bytes site/images/editor-highlight-2.png | Bin 0 -> 5846734 bytes site/images/engine-highlight-1.png | Bin 0 -> 4275831 bytes site/images/engine-highlight-2.png | Bin 0 -> 4066675 bytes site/images/engine-highlight-3.png | Bin 0 -> 8721787 bytes site/images/top_contributors/MioLogo.png | Bin 0 -> 89959 bytes site/images/untoldenginewhite.png | Bin 0 -> 3771 bytes site/index.html | 2401 ++++++ site/search/search_index.json | 1 + site/sitemap.xml | 187 + site/sitemap.xml.gz | Bin 0 -> 619 bytes 108 files changed, 123248 insertions(+), 97 deletions(-) create mode 100644 .github/workflows/docs.yml delete mode 100644 docs/01-Intro.md create mode 100644 docs/index.md create mode 100644 mkdocs.yml create mode 100644 site/404.html create mode 100644 site/API/GettingStarted/index.html create mode 100644 site/API/Optimizations/index.html create mode 100644 site/API/SpatialDebugger/index.html create mode 100644 site/API/UsageExamples/index.html create mode 100644 site/API/UsingAnimationSystem/index.html create mode 100644 site/API/UsingAsyncLoading/index.html create mode 100644 site/API/UsingCameraSystem/index.html create mode 100644 site/API/UsingGaussianSystem/index.html create mode 100644 site/API/UsingGeometryStreamingSystem/index.html create mode 100644 site/API/UsingInputSystem/index.html create mode 100644 site/API/UsingLOD-Batching-Streaming/index.html create mode 100644 site/API/UsingLODSystem/index.html create mode 100644 site/API/UsingLightingSystem/index.html create mode 100644 site/API/UsingMaterials/index.html create mode 100644 site/API/UsingPhysicsSystem/index.html create mode 100644 site/API/UsingPostFX/index.html create mode 100644 site/API/UsingProfiler/index.html create mode 100644 site/API/UsingRegistrationSystem/index.html create mode 100644 site/API/UsingRenderingSystem/index.html create mode 100644 site/API/UsingScenegraph/index.html create mode 100644 site/API/UsingSpatialInput/index.html create mode 100644 site/API/UsingStaticBatchingSystem/index.html create mode 100644 site/API/UsingSteeringSystem/index.html create mode 100644 site/API/UsingTheExporter/index.html create mode 100644 site/API/UsingTheLogger/index.html create mode 100644 site/API/UsingTransformSystem/index.html create mode 100644 site/API/UsingUntoldEngineCLI/index.html create mode 100644 site/Architecture/assetFormat/index.html create mode 100644 site/Architecture/assetProfiler/index.html create mode 100644 site/Architecture/asset_remote_streaming/index.html create mode 100644 site/Architecture/batchingSystem/index.html create mode 100644 site/Architecture/geometryStreamingSystem/index.html create mode 100644 site/Architecture/lodSystem/index.html create mode 100644 site/Architecture/meshResourceManager/index.html create mode 100644 site/Architecture/outOfCore/index.html create mode 100644 site/Architecture/progressiveAssetLoader/index.html create mode 100644 site/Architecture/renderingSystem/index.html create mode 100644 site/Architecture/streamingCacheLifecycle/index.html create mode 100644 site/Architecture/streamingRegionManager/index.html create mode 100644 site/Architecture/textureStreamingSystem/index.html create mode 100644 site/Architecture/tilebasedstreaming/index.html create mode 100644 site/Architecture/xrRenderingSystem/index.html create mode 100644 site/Contributor/ContributionGuidelines/index.html create mode 100644 site/Contributor/Formatting/index.html create mode 100644 site/Contributor/versioning/index.html create mode 100644 site/assets/images/favicon.png create mode 100644 site/assets/javascripts/bundle.79ae519e.min.js create mode 100644 site/assets/javascripts/bundle.79ae519e.min.js.map create mode 100644 site/assets/javascripts/lunr/min/lunr.ar.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.da.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.de.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.du.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.el.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.es.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.fi.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.fr.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.he.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.hi.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.hu.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.hy.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.it.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.ja.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.jp.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.kn.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.ko.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.multi.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.nl.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.no.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.pt.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.ro.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.ru.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.sa.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.sv.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.ta.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.te.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.th.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.tr.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.vi.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.zh.min.js create mode 100644 site/assets/javascripts/lunr/tinyseg.js create mode 100644 site/assets/javascripts/lunr/wordcut.js create mode 100644 site/assets/javascripts/workers/search.2c215733.min.js create mode 100644 site/assets/javascripts/workers/search.2c215733.min.js.map create mode 100644 site/assets/stylesheets/main.484c7ddc.min.css create mode 100644 site/assets/stylesheets/main.484c7ddc.min.css.map create mode 100644 site/assets/stylesheets/palette.ab4e12ef.min.css create mode 100644 site/assets/stylesheets/palette.ab4e12ef.min.css.map create mode 100644 site/images/editor-highlight-1.png create mode 100644 site/images/editor-highlight-2.png create mode 100644 site/images/engine-highlight-1.png create mode 100644 site/images/engine-highlight-2.png create mode 100644 site/images/engine-highlight-3.png create mode 100644 site/images/top_contributors/MioLogo.png create mode 100644 site/images/untoldenginewhite.png create mode 100644 site/index.html create mode 100644 site/search/search_index.json create mode 100644 site/sitemap.xml create mode 100644 site/sitemap.xml.gz diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..0171f2f4 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,23 @@ +name: Deploy Docs + +on: + push: + branches: + - develop + paths: + - 'docs/**' + - 'mkdocs.yml' + +permissions: + contents: write + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - run: pip install mkdocs-material + - run: mkdocs gh-deploy --force diff --git a/docs/01-Intro.md b/docs/01-Intro.md deleted file mode 100644 index 874c7528..00000000 --- a/docs/01-Intro.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -slug: /intro ---- - -# Untold Engine - -Untold Engine is an **open-source 3D engine written in Swift and powered by Metal**, designed for Apple platforms including **macOS, iOS, and visionOS**. - -The project focuses on building a **clean, system-driven architecture** with modern rendering, an ECS-based gameplay model, and an extensible asset pipeline. - -The engine is under active development and continues to evolve as new systems and workflows are added. - ---- - -## 🎯 Who is this for? - -Untold Engine is designed for developers who: - -- Want **full control over rendering and systems** -- Prefer working directly with **Swift + Metal** -- Are building **XR, 3D, or visualization applications** -- Need to handle **large scenes, streaming data, or custom pipelines** - -This is not a drag-and-drop editor-first engine — it is a **code-driven engine for developers who want to understand and shape the system**. - - -Creator & Lead Developer: -http://www.haroldserrano.com - ---- - -# 🚀 Try the Engine Right Now - -The fastest way to experience Untold Engine is to run the demo project. - -Clone the repository, run the engine and load a USDZ file: - -```bash -git clone https://github.com/untoldengine/UntoldEngine.git -cd UntoldEngine -swift run untolddemo -``` - -This will: - -- Build the engine using **Swift Package Manager** -- Compile the demo project -- Launch the demo so you can see the engine running immediately - -No additional setup is required. - ---- - -## 🧱 Core Direction - -Untold Engine is being developed with the following goals: - -- **Large Scene Rendering** - Striving to support LOD, geometry streaming, batching, and memory-aware systems for large datasets - -- **XR / visionOS Support** - Expanding support for spatial input, AR workflows, and Vision Pro experiences - -- **Metal-First Architecture** - Keeping the rendering layer close to Metal to maintain performance and control - ---- - -## 🖼 Example Use Cases - -Untold Engine aims to support applications such as: - -- XR applications (Vision Pro, ARKit-based apps) -- Large-scale scene visualization (cities, archviz, datasets) -- Custom rendering pipelines and experiments -- Simulation tools and interactive 3D systems diff --git a/docs/API/GettingStarted.md b/docs/API/GettingStarted.md index 5122e560..a0f5426a 100644 --- a/docs/API/GettingStarted.md +++ b/docs/API/GettingStarted.md @@ -40,7 +40,7 @@ used inside your game. [Download Untold Engine Studio](https://github.com/untoldengine/UntoldEditor/releases) -![untoldeditor-image-1](/docs/images/editor-highlight-1.png) +![untoldeditor-image-1](../images/editor-highlight-1.png) To set up a project: 1. Click on "New". diff --git a/docs/Contributor/ContributionGuidelines.md b/docs/Contributor/ContributionGuidelines.md index 5f52ebeb..20d82e07 100644 --- a/docs/Contributor/ContributionGuidelines.md +++ b/docs/Contributor/ContributionGuidelines.md @@ -1,9 +1,3 @@ ---- -id: contributorguidelines -title: Contributor Guidelines -sidebar_position: 1 ---- - # Contributing Guidelines Thank you for your interest in contributing! diff --git a/docs/Contributor/Formatting.md b/docs/Contributor/Formatting.md index 6d45d621..6baef2e3 100644 --- a/docs/Contributor/Formatting.md +++ b/docs/Contributor/Formatting.md @@ -1,10 +1,4 @@ ---- -id: formatting -title: Formatting -sidebar_position: 2 ---- - -# Formatting and Linting +# Formatting and Linting To maintain a consistent code style across the Untold Engine repo, we use [SwiftFormat](https://github.com/nicklockwood/SwiftFormat). SwiftFormat is a code formatter for Swift that helps enforce Swift style conventions and keep the codebase clean. If you don't have SwiftFormat installed, see the **Installing SwiftFormat** section below. diff --git a/docs/Contributor/versioning.md b/docs/Contributor/versioning.md index ddb6c7a9..7b138a73 100644 --- a/docs/Contributor/versioning.md +++ b/docs/Contributor/versioning.md @@ -1,10 +1,4 @@ ---- -id: versioning -title: Versioning -sidebar_position: 3 ---- - -# Versioning +# Versioning To help us identity the purpose of your commits, make sure to use the following tags in your commit messages. The tags will also automatically increment the the current version when pushed to github. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..37094682 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,174 @@ +# Untold Engine + +![Build Status](https://github.com/untoldengine/UntoldEngine/actions/workflows/ci-build-test.yml/badge.svg?style=flat-square) +[![Project license](https://img.shields.io/github/license/untoldengine/UntoldEngine.svg?style=flat-square)](https://github.com/untoldengine/UntoldEngine/blob/main/LICENSE) +[![Pull Requests welcome](https://img.shields.io/badge/PRs-welcome-ff69b4.svg?style=flat-square)](https://github.com/untoldengine/UntoldEngine/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) +![Version](https://img.shields.io/github/v/release/untoldengine/UntoldEngine?style=flat-square&label=version) +![Commits](https://img.shields.io/github/commit-activity/t/untoldengine/UntoldEngine?style=flat-square&label=commits) +![Last Commit](https://img.shields.io/github/last-commit/untoldengine/UntoldEngine?style=flat-square&label=last+commit) + +--- + +Untold Engine is a **Swift + Metal 3D engine for macOS, iOS, and visionOS** — with native Apple Vision Pro support and a growing focus on spatial computing — built for developers who: + +- Want **full control over rendering and systems** +- Prefer working directly with **Swift + Metal** +- Are building **XR, 3D, or visualization applications** +- Need to handle **large scenes, streaming data, or custom pipelines** + +If you've hit the ceiling of what existing engines allow on Apple platforms, this is for you. + +![untoldengine-image](images/engine-highlight-1.png) + +Creator & Lead Developer: +[Harold Serrano](http://www.haroldserrano.com) + +--- + +## Watch It in Action — Apple Vision Pro Demos + + + + + + + + + + + + +
+ +--- + +## Try the Engine Right Now + +The fastest way to experience Untold Engine is to run the demo project. + +Clone the repository and launch the demo: + +```bash +git clone https://github.com/untoldengine/UntoldEngine.git +cd UntoldEngine +swift run untolddemo +``` + +The demo UI lets you see the engine in action right away. Using the `Remote Scene` drop-down menu, you can choose a scene to stream directly into the demo through the engine's **Asset Remote Streaming** support. + +![untoldengine-image-2](images/engine-highlight-2.png) + +### I want to try my own USDZ + +Untold Engine uses its own native asset format: `.untold`. + +To try your own `USDZ` file, first convert it to `.untold` using the `Tools` section in the demo UI. + +After the export is complete, open the Local Scene `Browse` drop-down menu, choose `.untold`, then browse for and select your exported `.untold` file. + +> **Note:** The exporter requires [Blender](https://www.blender.org). + +--- + +![untoldengine-image-3](images/engine-highlight-3.png) + +## Getting Started + +To create your own project using the Untold Engine, see [Getting Started](API/GettingStarted.md). + +--- + +## Core Direction + +Untold Engine is built around three focused goals: + +- **Spatial Engine First** — Designed for spatial computing applications. LOD, geometry streaming, and static batching exist to support large, real-world-scale environments where presence and performance both matter. + +- **XR / visionOS Support** — Spatial input, AR workflows, and Vision Pro support are functional today and expanding with each release. + +- **Metal-First Architecture** — The rendering layer stays close to Metal to maintain performance and control, without abstraction layers getting in the way. + +--- + +## Example Use Cases + +Untold Engine is well-suited for: + +- XR applications (Vision Pro, ARKit-based apps) +- Large-scale scene visualization (cities, archviz, datasets) +- Custom rendering pipelines and experiments +- Simulation tools and interactive 3D systems + +--- + +## Current Features + +- **Apple Platform Coverage** — Unified Swift + Metal codebase for macOS, iOS, and visionOS +- **Rendering Pipeline** — Metal renderer with PBR/IBL workflows and post-processing across standard and XR paths +- **AR and XR Runtime Support** — Built-in AR workflows plus visionOS integration and spatial interaction support +- **ECS + Scene Graph Core** — Component-based architecture with hierarchical transforms and scene root transform controls +- **Async Content Loading** — Asynchronous loading pipeline for scenes and assets to improve responsiveness on large worlds +- **LOD and Streaming** — LOD support with geometry streaming, streaming regions, and memory budget management +- **Static Batching and Culling** — Static batching, octree acceleration, and occlusion culling for large-scene performance +- **Advanced Picking** — Scene, ground, and GPU ray picking with octree-backed intersection paths +- **Spatial Input Features** — XR spatial input helpers including anchored pinch drag, distance tracking, and two-hand rotation +- **Scripting System (USC)** — Untold Script Core with multi-script support plus camera, math, and physics APIs (Experimental) +- **Gameplay Systems** — Physics, animation, camera waypoint, and input systems (keyboard, mouse, touch, and gamepad) +- **Gaussian Splat Rendering** — Native Metal support for rendering and compositing 3D Gaussian content +- **Tooling Integration** — Optional Untold Editor workflow and Swift Package Manager integration + +--- + +## Roadmap + +See open issues for planned features and known improvements. + +- [Feature Requests](https://github.com/untoldengine/UntoldEngine/issues?q=label%3Aenhancement) +- [Bug Reports](https://github.com/untoldengine/UntoldEngine/issues?q=label%3Abug) + +--- + +## Contributing + +Contributions are welcome — whether that's fixing bugs, improving systems, writing documentation, or proposing ideas. + +Before submitting a pull request, please review the [Contributing Guidelines](Contributor/ContributionGuidelines.md). + +All contributions are licensed under **MPL-2.0**. + +--- + +## GitHub Sponsors + +A huge thanks to the people helping shape the Untold Engine. + +

+ + MioLabs + +

+ +--- + +## License + +Untold Engine is licensed under the **Mozilla Public License 2.0 (MPL-2.0)**. + +This allows developers to build commercial applications while ensuring improvements to the engine itself remain open. + +| Use Case | Allowed | Obligation | +|----------|---------|-----------| +| Build games | Yes | Game code can remain proprietary | +| Commercial apps | Yes | No royalties | +| Modify engine | Yes | Modified engine files remain MPL | +| Create plugins | Yes | Any license allowed | + +Full license: https://www.mozilla.org/MPL/2.0/ + +--- + +## Questions & Discussions + +- [GitHub Discussions](https://github.com/untoldengine/UntoldEngine/discussions) — ideas and questions +- [GitHub Issues](https://github.com/untoldengine/UntoldEngine/issues) — bugs and tasks +- [Ask a Question](https://github.com/untoldengine/UntoldEngine/issues/new?assignees=&labels=question&template=04_SUPPORT_QUESTION.md&title=support%3A+) diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..0adf70ed --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,93 @@ +site_name: Untold Engine +site_url: https://untoldengine.github.io/UntoldEngine/ +repo_url: https://github.com/untoldengine/UntoldEngine +repo_name: untoldengine/UntoldEngine + +docs_dir: docs + +theme: + name: material + logo: images/untoldenginewhite.png + favicon: images/untoldenginewhite.png + palette: + - scheme: slate + primary: black + accent: deep orange + toggle: + icon: material/brightness-4 + name: Switch to light mode + - scheme: default + primary: black + accent: deep orange + toggle: + icon: material/brightness-7 + name: Switch to dark mode + features: + - navigation.tabs + - navigation.sections + - navigation.top + - search.suggest + - search.highlight + - content.code.copy + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.superfences + - pymdownx.inlinehilite + - admonition + - pymdownx.details + - attr_list + - md_in_html + - tables + +nav: + - Introduction: index.md + - API: + - Getting Started: API/GettingStarted.md + - Usage Examples: API/UsageExamples.md + - Registration System: API/UsingRegistrationSystem.md + - Rendering System: API/UsingRenderingSystem.md + - Transform System: API/UsingTransformSystem.md + - Camera System: API/UsingCameraSystem.md + - Input System: API/UsingInputSystem.md + - Spatial Input: API/UsingSpatialInput.md + - Lighting System: API/UsingLightingSystem.md + - Materials: API/UsingMaterials.md + - Animation System: API/UsingAnimationSystem.md + - Physics System: API/UsingPhysicsSystem.md + - Steering System: API/UsingSteeringSystem.md + - Post Effects: API/UsingPostFX.md + - Scene Graph: API/UsingScenegraph.md + - LOD System: API/UsingLODSystem.md + - Static Batching: API/UsingStaticBatchingSystem.md + - LOD + Batching + Streaming: API/UsingLOD-Batching-Streaming.md + - Geometry Streaming: API/UsingGeometryStreamingSystem.md + - Async Loading: API/UsingAsyncLoading.md + - Gaussian System: API/UsingGaussianSystem.md + - Profiler: API/UsingProfiler.md + - Spatial Debugger: API/SpatialDebugger.md + - Logger: API/UsingTheLogger.md + - Exporter: API/UsingTheExporter.md + - Untold Engine CLI: API/UsingUntoldEngineCLI.md + - Optimizations: API/Optimizations.md + - Architecture: + - Rendering System: Architecture/renderingSystem.md + - XR Rendering System: Architecture/xrRenderingSystem.md + - Out-of-Core Geometry: Architecture/outOfCore.md + - Batching System: Architecture/batchingSystem.md + - LOD System: Architecture/lodSystem.md + - Tile-Based Streaming: Architecture/tilebasedstreaming.md + - Streaming Region Manager: Architecture/streamingRegionManager.md + - Streaming Cache Lifecycle: Architecture/streamingCacheLifecycle.md + - Geometry Streaming System: Architecture/geometryStreamingSystem.md + - Texture Streaming System: Architecture/textureStreamingSystem.md + - Mesh Resource Manager: Architecture/meshResourceManager.md + - Asset Format: Architecture/assetFormat.md + - Asset Profiler: Architecture/assetProfiler.md + - Progressive Asset Loader: Architecture/progressiveAssetLoader.md + - Asset Remote Streaming: Architecture/asset_remote_streaming.md + - Contributing: + - Guidelines: Contributor/ContributionGuidelines.md + - Versioning: Contributor/versioning.md + - Formatting: Contributor/Formatting.md diff --git a/site/404.html b/site/404.html new file mode 100644 index 00000000..37d46ec1 --- /dev/null +++ b/site/404.html @@ -0,0 +1,1875 @@ + + + + + + + + + + + + + + + + + + + + + + Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ +

404 - Not found

+ +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/GettingStarted/index.html b/site/API/GettingStarted/index.html new file mode 100644 index 00000000..d0ccc7b3 --- /dev/null +++ b/site/API/GettingStarted/index.html @@ -0,0 +1,2571 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Getting Started - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Getting Started

+

This guide takes you from a fresh Untold Engine checkout to a working game +project. You will run the demo, create an Xcode project, export assets into the +engine's .untold runtime format, and load those assets from a GameScene.

+

You can create projects and export assets in two ways:

+
    +
  • Use the CLI if you prefer terminal commands or want a repeatable workflow.
  • +
  • Use Untold Engine Studio if you prefer a visual editor for project setup, + asset import, and scene preparation.
  • +
+

After your project is created, both workflows lead to the same place: an Xcode +project with a GameData folder that contains the assets your game loads at +runtime.

+
+

Clone the Untold Engine

+

Clone the repository and launch the demo:

+
git clone https://github.com/untoldengine/UntoldEngine.git
+cd UntoldEngine
+swift run untolddemo
+
+

Create an Xcode Project

+

You can create a project with either Untold Engine Studio or the CLI. If you are +new to Untold Engine, start with the Editor. If you prefer terminal workflows or +want repeatable project setup, use the CLI.

+

Option 1: Editor

+

Use Untold Engine Studio for a visual workflow. It is a standalone editor for +creating projects, preparing assets, composing scenes, and generating scene files +used inside your game.

+

Download Untold Engine Studio

+

untoldeditor-image-1

+

To set up a project: +1. Click on "New". +2. Provide a Project name +3. Provide a Bundle name +4. Select the Target Platform +5. Provide an output path

+

Untold Engine Studio will create an Xcode project ready to be used with Untold Engine.

+

Option 2: CLI

+

Use untoldengine-create to generate a ready-to-run Xcode project with Untold Engine wired in.

+

Install it from the repository:

+
./scripts/install-untoldengine-create.sh
+
+

Now create an Xcode project. The example below uses --platform visionos to +create a Vision Pro project.

+

Vision Pro Example

+
cd ~/Projects
+untoldengine-create create VisionGame --platform visionos
+open VisionGame/VisionGame.xcodeproj
+
+

If you want to create a project for other platforms, you can use the flags below:

+

Platform options

+
# visionOS (Apple Vision Pro)
+untoldengine-create create MyGame --platform visionos
+
+# macOS (default)
+untoldengine-create create MyGame --platform macos
+
+# iOS with ARKit
+untoldengine-create create MyGame --platform ios-ar
+
+# iOS
+untoldengine-create create MyGame --platform ios
+
+

Dependency behavior by platform:

+
    +
  • visionos: UntoldEngineXR + UntoldEngineAR
  • +
  • ios-ar: UntoldEngineAR
  • +
  • ios and macos: UntoldEngine
  • +
+

Native Asset Format: .untold

+

Untold Engine uses .untold as its native runtime asset format. USDZ/USD remains +the authoring format — you model assets in your DCC tool, export to USDZ, then +convert to .untold before loading them in the engine.

+

The .untold format is a binary container optimised for fast runtime parsing with +no ModelIO dependency. It supports runtime mesh data, PBR materials, texture references, +transforms, bounds, and exported animation clips.

+
+

Note: The exporter requires Blender.

+
+

You can convert assets with either Untold Engine Studio or the CLI. If you are +new to Untold Engine, start with the Editor. If you prefer terminal workflows or +need repeatable asset export commands, use the CLI.

+

Option 1: Editor

+

To convert a USDZ file into the .untold format using the editor:

+
    +
  1. Click on "Import" in the Asset Browser View.
  2. +
  3. Click on "Import Models"
  4. +
  5. Find a USDZ file you want to convert
  6. +
  7. Click on Export
  8. +
  9. When the export has completed, you will see your new .untold model under the Model Category
  10. +
+

At this point, head over to your Xcode project. You will also notice that your .untold model is under Sources/<ProjectName>/GameData/Models.

+

Option 2: CLI

+

Use the export-untold script to convert a single USDZ asset:

+
./scripts/export-untold \
+  --input /path/to/your/model/robot/robot.usdz \
+  --output /path/to/your/project/GameData/Models/robot/robot.untold \
+  --ConvertOrientation \
+  --source-orientation blender-native
+
+

For animation assets, use the --animation flag:

+
./scripts/export-untold \
+  --input /path/to/your/animation/robot/robot.usdz \
+  --output /path/to/your/project/GameData/Animations/robot/robot.untold \
+  --ConvertOrientation \
+  --source-orientation blender-native \
+  --animation
+
+

For large scenes that need tile-based streaming, use export-untold-tiles to +partition the scene and generate a manifest JSON:

+
./scripts/export-untold-tiles \
+  --input /path/to/your/model/dungeon/dungeon.usdz \
+  --output-dir /path/to/your/project/GameData/StreamModels/dungeon/tile_exports \
+  --tile-size-x 25 \
+  --tile-size-z 25 \
+  --generate-hlod \
+  --generate-lod
+
+

For the full list of options, validation flags, and expected output layout see +Using The Exporter. For optional asset optimization +workflows, see Optimizations.

+
+

Loading a Single Asset

+

Once in your Xcode project, head over to the init function in Sources//GameScene.swift.

+

Use setEntityMeshAsync to load an .untold file as an always-resident asset. +This is the right choice for props, characters, and any object that should stay +in memory for the lifetime of the scene.

+
//...After configureEngineSystems()
+
+let entity = createEntity()
+setEntityName(entityId: entity, name: "robot")
+
+setEntityMeshAsync(entityId: entity, filename: "robot", withExtension: "untold") { success in
+    if success {
+        translateBy(entityId: entity, position: simd_float3(0.0, 0.0, 0.0))
+        setEntityKinetics(entityId: entity)
+    }
+    setSceneReady(success)
+}
+
+

setEntityMeshAsync is non-blocking. The completion block fires on the main thread +once the mesh is parsed and uploaded to GPU memory.

+
+

Loading a Streamed Scene

+

Use setEntityStreamScene to load a large scene that streams tiles in and out of +GPU memory based on camera proximity. Pass either a local manifest path or a remote +https:// URL — the engine handles downloading and caching automatically.

+
//..After configureEngineSystems()
+
+
+let sceneRoot = createEntity()
+setEntityName(entityId: sceneRoot, name: "dungeon")
+
+// Local manifest
+setEntityStreamScene(entityId: sceneRoot, manifest: "dungeon", withExtension: "json") { success in
+    setSceneReady(success)
+}
+
+

Loading a Remote Streamed Scene

+

To streame a remote scene, you use the same function setEntityStreamedScene() but provide a url to your manifest json file.

+
// Remote manifest (downloaded and cached on demand)
+if let url = URL(string: "https://cdn.example.com/dungeon/dungeon.json") {
+    setEntityStreamScene(entityId: sceneRoot, url: url) { success in
+        setSceneReady(success)
+    }
+}
+
+

setEntityStreamScene registers lightweight stub entities for every tile in the +manifest, all parented under sceneRoot (no geometry is parsed at this point). +GeometryStreamingSystem then loads and unloads tile geometry as the camera moves. +See Tile-Based Streaming for the full streaming +architecture.

+
+

Legacy overloadsloadTiledScene(manifest:) and loadTiledScene(url:) remain +available for backwards compatibility. They create an internal root entity automatically.

+
+
+

Finding Entities in the Loaded Scene

+

Retrieve a named entity with findEntity(name:) inside the completion block or +after setSceneReady:

+
setEntityMeshAsync(entityId: entity, filename: "stadium", withExtension: "untold") { success in
+    if let player = findEntity(name: "player") {
+        rotateTo(entityId: player, angle: 0, axis: simd_float3(0.0, 1.0, 0.0))
+        setEntityKinetics(entityId: player)
+    }
+    setSceneReady(success)
+}
+
+
+

Camera and Lighting

+

Create a camera and directional light manually in your scene setup, then position +the camera after assets load:

+
let gameCamera = createEntity()
+setEntityName(entityId: gameCamera, name: "Main Camera")
+createGameCamera(entityId: gameCamera)
+CameraSystem.shared.activeCamera = gameCamera
+
+let light = createEntity()
+setEntityName(entityId: light, name: "Directional Light")
+createDirLight(entityId: light)
+
+

After loading:

+
moveCameraTo(entityId: findGameCamera(), 0.0, 3.0, 10.0)
+ambientIntensity = 0.4
+
+
+

Putting It All Together

+

A complete GameScene using the patterns above:

+
final class GameScene {
+
+    init() {
+        // Camera and light
+        let gameCamera = createEntity()
+        setEntityName(entityId: gameCamera, name: "Main Camera")
+        createGameCamera(entityId: gameCamera)
+        CameraSystem.shared.activeCamera = gameCamera
+
+        let light = createEntity()
+        setEntityName(entityId: light, name: "Directional Light")
+        createDirLight(entityId: light)
+
+        // Load a single always-resident asset
+        let stadium = createEntity()
+
+        setEntityMeshAsync(entityId: stadium, filename: "stadium", withExtension: "untold") { success in
+            if let player = findEntity(name: "player") {
+                rotateTo(entityId: player, angle: 0, axis: simd_float3(0.0, 1.0, 0.0))
+                setEntityAnimations(entityId: player, filename: "running", withExtension: "untold", name: "running")
+                setEntityAnimations(entityId: player, filename: "idle",    withExtension: "untold", name: "idle")
+                setEntityKinetics(entityId: player)
+            }
+
+            moveCameraTo(entityId: findGameCamera(), 0.0, 3.0, 10.0)
+            ambientIntensity = 0.4
+            setSceneReady(success)
+        }
+    }
+}
+
+

For a large streaming scene, replace the setEntityMeshAsync call with setEntityStreamScene:

+
let sceneRoot = createEntity()
+setEntityName(entityId: sceneRoot, name: "dungeon")
+
+setEntityStreamScene(entityId: sceneRoot, manifest: "dungeon", withExtension: "json") { success in
+    moveCameraTo(entityId: findGameCamera(), 0.0, 3.0, 10.0)
+    ambientIntensity = 0.4
+    setSceneReady(success)
+}
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/Optimizations/index.html b/site/API/Optimizations/index.html new file mode 100644 index 00000000..4042e058 --- /dev/null +++ b/site/API/Optimizations/index.html @@ -0,0 +1,2320 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Optimizations - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Optimizations

+

Untold Engine supports optional asset optimization workflows that reduce runtime +memory, file size, and streaming cost. These steps are usually applied after +exporting assets to .untold.

+

For exporter commands, flags, and expected output layout, see +Using The Exporter.

+

ASTC Texture Compression

+

ASTC is a GPU-native block-compression format supported on all Apple Silicon and A-series devices. Converting textures to ASTC reduces texture memory by 4-8x compared to uncompressed RGBA8 and eliminates CPU-side decode. The GPU receives the compressed blocks directly.

+

ASTC compression is a post-export step run with texbake.py, separate from the exporters. This keeps the exporter's Blender Python environment free of extra dependencies.

+

Prerequisites

+

Install astcenc. The tool is resolved in this order:

+
    +
  1. ASTCENC_BIN=/path/to/astcenc
  2. +
  3. Tools/astcenc/astcenc beside the repo root
  4. +
  5. astcenc on PATH
  6. +
+

Single asset workflow

+
# 1. Export the asset
+./scripts/export-untold \
+  --input GameData/Models/robot/robot.usdz \
+  --output GameData/Models/robot/robot.untold
+
+# 2. Bake all textures in the Textures/ directory to .utex
+python3 scripts/texbake.py --dir GameData/Models/robot/Textures/
+
+# 3. Patch the .untold file to reference the .utex files
+python3 scripts/texbake.py --patch-refs GameData/Models/robot/robot.untold
+
+

Tile export workflow

+

Tile exports produce many .untold files. Pass the tile output directory to --patch-refs and all .untold files inside are patched in one go:

+
# 1. Export tiles
+./scripts/export-untold-tiles \
+  --input GameData/Models/dungeon/dungeon.usdz \
+  --output-dir GameData/Models/dungeon/tile_exports
+
+# 2. Bake all textures
+python3 scripts/texbake.py --dir GameData/Models/dungeon/tile_exports/Textures/
+
+# 3. Patch all .untold files in the tile directory
+python3 scripts/texbake.py --patch-refs GameData/Models/dungeon/tile_exports/
+
+

What the bake step does

+
    +
  1. Converts every texture in the directory to .utex using astcenc:
  2. +
  3. Base color and emissive: ASTC 4x4 sRGB
  4. +
  5. Normal maps: ASTC 4x4 LDR
  6. +
  7. Roughness, metallic, occlusion: ASTC 6x6 LDR
  8. +
  9. Rewrites the .untold binary to point each texture reference at the new .utex file and sets the textureFormat field to the correct ASTC variant.
  10. +
  11. The original PNG/JPEG files are left in place. The engine loads .utex when present and falls back to PNG/JPEG otherwise.
  12. +
+

Single-file bake with slot override

+

For cases where filename-based slot detection is insufficient, pass --input and --slot directly:

+
python3 scripts/texbake.py \
+  --input GameData/Models/robot/Textures/surface_data.png \
+  --slot roughness
+
+

Available slots: base_color, normal, roughness, metallic, occlusion, orm, emissive, opacity, data.

+

LZ4 Geometry Compression

+

Pass --compress-geometry to export-untold or export-untold-tiles to compress the vertex and index chunks of the output .untold file with LZ4.

+

Prerequisites

+

Install the Python LZ4 package:

+
pip install lz4
+
+

Single asset

+
./scripts/export-untold \
+  --input GameData/Models/robot/robot.usdz \
+  --output GameData/Models/robot/robot.untold \
+  --compress-geometry
+
+

Tile export

+
./scripts/export-untold-tiles \
+  --input GameData/Models/dungeon/dungeon.usdz \
+  --output-dir GameData/Models/dungeon/tile_exports \
+  --tile-size-x 25 \
+  --tile-size-y 10000 \
+  --tile-size-z 25 \
+  --compress-geometry
+
+

What compression does

+
    +
  • Only the vertex_data and index_data chunks are compressed. Metadata chunks (string table, entity table, mesh table, material table, texture table) are always stored uncompressed.
  • +
  • The compressed format is LZ4 raw block (lz4.block, not lz4.frame), which matches Apple's COMPRESSION_LZ4_RAW algorithm used by the runtime decompressor.
  • +
  • Both the compressed size and the original uncompressed size are recorded in each chunk entry, so the runtime can allocate the exact decompression buffer without an extra read.
  • +
  • The content hash in the file header is computed over the compressed bytes, consistent with runtime validation.
  • +
+

Compression is compatible with all other flags including --validate, --generate-hlod, --generate-lod, and the ASTC texture bake workflow.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/SpatialDebugger/index.html b/site/API/SpatialDebugger/index.html new file mode 100644 index 00000000..b2b936f3 --- /dev/null +++ b/site/API/SpatialDebugger/index.html @@ -0,0 +1,2536 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Spatial Debugger - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Spatial Debugger

+

The Spatial Debugger provides visual overlays that help diagnose +spatial systems in the engine, including:

+
    +
  • Octree spatial partitioning
  • +
  • Streaming / residency behavior
  • +
  • Frustum culling decisions
  • +
  • LOD selection
  • +
+

It renders wireframe octree leaf bounds and can color them based on +runtime state to help identify issues such as:

+
    +
  • incorrect spatial partitioning
  • +
  • streaming thrashing
  • +
  • over-resident regions
  • +
  • unexpected culling
  • +
  • incorrect LOD selection
  • +
+

This tool is intended for debugging large scenes and spatial performance +issues.

+
+

Octree Bounds Visualization

+

The Spatial Debugger renders octree leaf bounds as wireframe boxes.

+

Each leaf represents a spatial region managed by the engine's octree +system.

+

Leaf coloring can be configured to visualize:

+
    +
  • plain structure
  • +
  • streaming/residency state
  • +
  • runtime culling state
  • +
+
+

Quick Start

+
import UntoldEngine
+
+// Optional: tighten world bounds to your scene for better visualization.
+OctreeSystem.shared.worldBounds = AABB(
+    min: simd_float3(-40, -5, -40),
+    max: simd_float3(40, 25, 40)
+)
+
+// Enable octree debug rendering.
+setOctreeLeafBoundsDebug(
+    enabled: true,
+    maxLeafNodeCount: 0,   // 0 = unlimited
+    occupiedOnly: true,    // draw only leaves containing entries
+    colorMode: .culling
+)
+
+

Disable the spatial debugger:

+
disableSpatialDebugVisualization()
+
+
+

Visualization Modes

+

Plain (structure only)

+

Draws octree leaf bounds in a single color.

+

Useful for verifying:

+
    +
  • octree subdivision
  • +
  • spatial partitioning accuracy
  • +
  • entity placement inside the tree
  • +
+
setOctreeLeafBoundsDebug(
+    enabled: true,
+    maxLeafNodeCount: 0,
+    occupiedOnly: true,
+    colorMode: .plain
+)
+
+
+

Residency (Streaming / LOD)

+

Colors leaves based on asset residency and streaming state.

+

Useful for diagnosing:

+
    +
  • streaming radius problems
  • +
  • assets that remain loaded too long
  • +
  • streaming thrashing
  • +
+
setOctreeLeafBoundsDebug(
+    enabled: true,
+    maxLeafNodeCount: 0,
+    occupiedOnly: true,
+    colorMode: .residency
+)
+
+

Color meanings:

+

Color Meaning

+
+

Green assets resident + Yellow loading/unloading + Red unloaded + Orange mixed residency states within the leaf + White no residency signal found

+

Residency information is derived from:

+
    +
  • StreamingComponent
  • +
  • LODComponent
  • +
+

If these components are missing, the leaf falls back to white.

+
+

Culling

+

Colors leaves based on runtime visibility.

+

Useful for diagnosing:

+
    +
  • frustum culling issues
  • +
  • objects unexpectedly culled
  • +
  • visibility system behavior
  • +
+
setOctreeLeafBoundsDebug(
+    enabled: true,
+    maxLeafNodeCount: 0,
+    occupiedOnly: true,
+    colorMode: .culling
+)
+
+

Color meanings:

+

Color Meaning

+
+

Green entity visible this frame + Blue entity culled this frame + Gray entity hidden (RenderComponent.isVisible == false) + Orange leaf contains mixed visibility states + White no culling signal found

+

Culling colors are evaluated per leaf each frame, using:

+
    +
  • visibleEntityIds
  • +
  • RenderComponent.isVisible
  • +
+

Because visibility updates every frame, colors may change as the camera +moves.

+
+

Showing Empty Leaves

+

To visualize the full octree structure including empty regions:

+
setOctreeLeafBoundsDebug(
+    enabled: true,
+    maxLeafNodeCount: 0,
+    occupiedOnly: false,
+    colorMode: .residency
+)
+
+

This can help diagnose:

+
    +
  • oversized nodes
  • +
  • uneven spatial subdivision
  • +
  • empty regions that remain allocated
  • +
+
+

API

+

setOctreeLeafBoundsDebug

+
setOctreeLeafBoundsDebug(
+    enabled: Bool,
+    maxLeafNodeCount: Int,
+    occupiedOnly: Bool,
+    colorMode: SpatialDebugColorMode
+)
+
+

Parameters:

+

Parameter Description

+
+

enabled Master toggle for octree visualization + maxLeafNodeCount Maximum leaves drawn per frame (0 = unlimited) + occupiedOnly When true, only leaves containing entries are drawn + colorMode Controls how leaf bounds are colored

+

Available color modes:

+
.plain
+.residency
+.culling
+
+
+

disableSpatialDebugVisualization

+

Disables all spatial debugging overlays.

+
disableSpatialDebugVisualization()
+
+
+

Runtime Behavior

+

The spatial debugger:

+
    +
  • runs in a dedicated spatial debug render pass
  • +
  • renders after transparency
  • +
  • uses depth testing
  • +
  • does not write depth
  • +
+

This ensures the visualization remains readable without interfering with +scene rendering.

+

Default behavior:

+
    +
  • leaves only
  • +
  • occupied leaves only
  • +
  • white wireframe bounds
  • +
+
+

Console Status Output

+

When enabled, the renderer periodically prints a status line:

+

This provides quick feedback that the system is active and indicates:

+
    +
  • total octree leaf count
  • +
  • number of leaves currently drawn
  • +
  • whether the draw cap is limiting output
  • +
+
+

LOD Visualizer

+

The engine also provides an LOD visualizer to display which LOD +level each renderable is currently using.

+

Enable it with:

+
setLODLevelDebug(enabled: true)
+
+

This mode colors renderables by their active LOD level to help diagnose:

+
    +
  • incorrect LOD thresholds
  • +
  • objects stuck in high-detail LODs
  • +
  • aggressive LOD switching
  • +
  • spatial LOD distribution across the scene
  • +
+
+ +

When diagnosing spatial performance issues, a typical workflow is:

+
    +
  1. Plain mode
      +
    • Verify octree subdivision
    • +
    +
  2. +
  3. Residency mode
      +
    • Confirm streaming behavior
    • +
    +
  4. +
  5. Culling mode
      +
    • Validate visibility decisions
    • +
    +
  6. +
  7. LOD visualizer
      +
    • Check LOD distribution
    • +
    +
  8. +
+

Together these tools provide a full picture of how the engine is +managing spatial data.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsageExamples/index.html b/site/API/UsageExamples/index.html new file mode 100644 index 00000000..56379279 --- /dev/null +++ b/site/API/UsageExamples/index.html @@ -0,0 +1,2453 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Usage Examples - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Usage Examples

+

This page collects common Untold Engine setup patterns in one place. Use it when +you want a concrete example first, then open the focused API pages for details.

+

For a broader first project walkthrough, see Getting Started.

+

Export Assets First

+

Untold Engine uses .untold as its preferred runtime asset format. Keep USD/USDZ +as your authoring format, then export it before loading it in the engine.

+

Convert one asset:

+
./scripts/export-untold \
+  --input GameData/Models/robot/robot.usdz \
+  --output GameData/Models/robot/robot.untold \
+  --ConvertOrientation \
+  --source-orientation blender-native
+
+

Export an animation clip:

+
./scripts/export-untold \
+  --input GameData/Models/robot/running.usdz \
+  --output GameData/Models/robot/running.untold \
+  --ConvertOrientation \
+  --source-orientation blender-native \
+  --animation
+
+

Export a large streamed scene:

+
./scripts/export-untold-tiles \
+  --input GameData/Models/city/city.usdz \
+  --output-dir GameData/Models/city/tile_exports \
+  --tile-size-x 25 \
+  --tile-size-z 25 \
+  --generate-hlod \
+  --generate-lod
+
+

For exporter options and expected output layout, see +Using The Exporter. For texture compression and LZ4 +compression, see Optimizations.

+

Create an Entity

+

Entities are lightweight IDs. Systems add behavior by attaching components or by +calling helper APIs that register the right components for you.

+
let robot = createEntity()
+setEntityName(entityId: robot, name: "robot")
+
+

For direct entity/component lifecycle APIs, see +Using the Registration System.

+

Load a Mesh Immediately

+

Use setEntityMesh when you want a small asset loaded immediately on the calling +thread. This is useful for simple examples, tiny props, tests, and editor-style +setup.

+
let crate = createEntity()
+setEntityName(entityId: crate, name: "crate")
+
+setEntityMesh(entityId: crate, filename: "crate", withExtension: "untold")
+translateTo(entityId: crate, position: simd_float3(0.0, 0.0, 0.0))
+
+

For most game scene setup, prefer setEntityMeshAsync so loading does not stall +the render loop.

+

Load a Mesh Asynchronously

+

Use setEntityMeshAsync when a model is large enough that loading it on the +calling thread could stall the engine. The asset still becomes always-resident +after it loads, but parsing and GPU upload happen asynchronously so the render +loop can keep running.

+
setSceneReady(false)
+
+let robot = createEntity()
+setEntityName(entityId: robot, name: "robot")
+
+setEntityMeshAsync(entityId: robot, filename: "robot", withExtension: "untold") { success in
+    if success {
+        translateTo(entityId: robot, position: simd_float3(0.0, 0.0, 0.0))
+        rotateTo(entityId: robot, angle: 0.0, axis: simd_float3(0.0, 1.0, 0.0))
+    }
+
+    setSceneReady(success)
+}
+
+

setEntityMeshAsync calls the completion block after parsing and GPU upload +finish. Apply dependent transforms, physics setup, animation setup, and batching +inside the completion block when they depend on the loaded mesh.

+

For loading behavior and progress APIs, see +Using Async Loading.

+

Add Camera and Lighting

+

Most examples need a game camera and at least one light.

+
let camera = createEntity()
+setEntityName(entityId: camera, name: "Main Camera")
+createGameCamera(entityId: camera)
+CameraSystem.shared.activeCamera = camera
+moveCameraTo(entityId: camera, 0.0, 3.0, 10.0)
+
+let sun = createEntity()
+setEntityName(entityId: sun, name: "Sun")
+createDirLight(entityId: sun)
+
+ambientIntensity = 0.4
+
+

For camera controls and path following, see +Using the Camera System. For light types, see +Using the Lighting System.

+

Play an Animation

+

Load the rigged mesh, then register one or more exported animation clips on the +same entity. Use changeAnimation to choose the active clip.

+
let player = createEntity()
+setEntityName(entityId: player, name: "player")
+
+setEntityMeshAsync(entityId: player, filename: "redplayer", withExtension: "untold") { success in
+    guard success else {
+        setSceneReady(false)
+        return
+    }
+
+    setEntityAnimations(entityId: player, filename: "running", withExtension: "untold", name: "running")
+    setEntityAnimations(entityId: player, filename: "idle", withExtension: "untold", name: "idle")
+
+    changeAnimation(entityId: player, name: "idle")
+    setSceneReady(true)
+}
+
+

Switch animations later:

+
changeAnimation(entityId: player, name: "running")
+
+

Pause or resume the current animation:

+
pauseAnimationComponent(entityId: player, isPaused: true)
+pauseAnimationComponent(entityId: player, isPaused: false)
+
+

For the full animation workflow, see +Using the Animation System.

+

Add Physics

+

Call setEntityKinetics after creating or loading the entity. Then configure mass, +gravity, or forces as needed.

+
let ball = createEntity()
+setEntityName(entityId: ball, name: "ball")
+
+setEntityMeshAsync(entityId: ball, filename: "ball", withExtension: "untold") { success in
+    guard success else {
+        setSceneReady(false)
+        return
+    }
+
+    translateTo(entityId: ball, position: simd_float3(0.0, 2.0, 0.0))
+    setEntityKinetics(entityId: ball)
+    setMass(entityId: ball, mass: 1.0)
+    setGravityScale(entityId: ball, gravityScale: 1.0)
+
+    setSceneReady(true)
+}
+
+

Apply a force when a gameplay event happens:

+
applyForce(entityId: ball, force: simd_float3(0.0, 5.0, 0.0))
+
+

For physics properties and steering helpers, see +Using the Physics System and +Using the Steering System.

+

Load a Streamed Scene

+

Use setEntityStreamScene for large worlds, cities, terrain, and remote streamed +scenes. The manifest is generated by export-untold-tiles.

+
setSceneReady(false)
+
+let sceneRoot = createEntity()
+setEntityName(entityId: sceneRoot, name: "city")
+
+setEntityStreamScene(entityId: sceneRoot, manifest: "city", withExtension: "json") { success in
+    if success {
+        moveCameraTo(entityId: findGameCamera(), 0.0, 4.0, 12.0)
+        ambientIntensity = 0.4
+    }
+
+    setSceneReady(success)
+}
+
+

Load a remote manifest:

+
let sceneRoot = createEntity()
+setEntityName(entityId: sceneRoot, name: "city")
+
+if let url = URL(string: "https://cdn.example.com/city/city.json") {
+    setEntityStreamScene(entityId: sceneRoot, url: url) { success in
+        setSceneReady(success)
+    }
+}
+
+

Do not attach StreamingComponent manually for app-level streaming. The tiled +scene pipeline creates and manages the streaming components internally.

+

For the public streaming API, see +Using Geometry Streaming. For the deeper +architecture, see Tile-Based Streaming.

+

Complete Small Scene

+

This example creates a camera, light, animated player, and physics-enabled ball.

+
final class GameScene {
+
+    private var pendingLoads = 2
+    private var sceneFailed = false
+
+    init() {
+        setSceneReady(false)
+        setupCameraAndLight()
+        loadPlayer()
+        loadBall()
+    }
+
+    private func setupCameraAndLight() {
+        let camera = createEntity()
+        setEntityName(entityId: camera, name: "Main Camera")
+        createGameCamera(entityId: camera)
+        CameraSystem.shared.activeCamera = camera
+        moveCameraTo(entityId: camera, 0.0, 3.0, 10.0)
+
+        let sun = createEntity()
+        setEntityName(entityId: sun, name: "Sun")
+        createDirLight(entityId: sun)
+
+        ambientIntensity = 0.4
+    }
+
+    private func loadPlayer() {
+        let player = createEntity()
+        setEntityName(entityId: player, name: "player")
+
+        setEntityMeshAsync(entityId: player, filename: "redplayer", withExtension: "untold") { success in
+            if success {
+                translateTo(entityId: player, position: simd_float3(0.0, 0.0, 0.0))
+                setEntityAnimations(entityId: player, filename: "idle", withExtension: "untold", name: "idle")
+                setEntityAnimations(entityId: player, filename: "running", withExtension: "untold", name: "running")
+                changeAnimation(entityId: player, name: "idle")
+                setEntityKinetics(entityId: player)
+            }
+
+            self.finishLoad(success)
+        }
+    }
+
+    private func loadBall() {
+        let ball = createEntity()
+        setEntityName(entityId: ball, name: "ball")
+
+        setEntityMeshAsync(entityId: ball, filename: "ball", withExtension: "untold") { success in
+            if success {
+                translateTo(entityId: ball, position: simd_float3(2.0, 1.0, 0.0))
+                setEntityKinetics(entityId: ball)
+                setMass(entityId: ball, mass: 1.0)
+                setGravityScale(entityId: ball, gravityScale: 1.0)
+            }
+
+            self.finishLoad(success)
+        }
+    }
+
+    private func finishLoad(_ success: Bool) {
+        if !success {
+            sceneFailed = true
+        }
+
+        pendingLoads -= 1
+
+        if pendingLoads == 0 {
+            setSceneReady(!sceneFailed)
+        }
+    }
+}
+
+ + + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingAnimationSystem/index.html b/site/API/UsingAnimationSystem/index.html new file mode 100644 index 00000000..0e9ef73f --- /dev/null +++ b/site/API/UsingAnimationSystem/index.html @@ -0,0 +1,2205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Animation System - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Enabling Animation in Untold Engine

+

The Untold Engine simplifies adding animations to your rigged models, allowing for lifelike movement and dynamic interactions. This guide will show you how to set up and play animations for a rigged model.

+

How to Enable Animation

+

Step 1: Create an Entity

+

Start by creating an entity to represent your animated model.

+
let redPlayer = createEntity()
+
+
+ +

Load your rigged model's .untold runtime asset and link it to the entity. This step ensures the entity is visually represented in the scene.

+
setEntityMesh(entityId: redPlayer, filename: "redplayer", withExtension: "untold", flip: false)
+
+
+
+
+

Note: If your model renders with the wrong orientation, set the flip parameter to false.

+
+
+
+
+

Step 3: Load the Animation

+

Load the animation data for your model by providing the exported animation .untold file and a name to reference the animation later.

+
setEntityAnimations(entityId: redPlayer, filename: "running", withExtension: "untold", name: "running")
+
+
+

Step 4: Set the Animation to play

+

Trigger the animation by referencing its name. This will set the animation to play on the entity.

+
changeAnimation(entityId: redPlayer, name: "running")
+
+
+

Step 5. Pause the animation (Optional)

+

To pause the current animation, simply call the following function. The animation component will be paused for the current entity.

+
pauseAnimationComponent(entityId: redPlayer, isPaused: true)
+
+
+

Running the Animation

+

Once the animation is set up:

+
    +
  1. Run the project: Your model will appear in the game window.
  2. +
  3. Click on "Play" to enter Game Mode:
  4. +
  5. The model will play the assigned animation in real time.
  6. +
+
+

Tips and Best Practices

+
    +
  • Name Animations Clearly: Use descriptive names like "running" or "jumping" to make it easier to manage multiple animations.
  • +
  • Debug Orientation Issues: If the model’s animation appears misaligned, revisit the flip parameter or check the model’s export settings.
  • +
  • Combine Animations: For complex behaviors, load multiple animations (e.g., walking, idle, jumping) and switch between them dynamically.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingAsyncLoading/index.html b/site/API/UsingAsyncLoading/index.html new file mode 100644 index 00000000..eae2aa06 --- /dev/null +++ b/site/API/UsingAsyncLoading/index.html @@ -0,0 +1,2425 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Async Loading - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Async Loading System

+

UntoldEngine loads meshes asynchronously so scene setup does not stall the render loop. Use native .untold assets for runtime geometry; legacy USD/USDZ runtime paths are still supported.

+

What the API Does

+

setEntityMeshAsync is the primary async asset-loading API for always-resident assets:

+
let entity = createEntity()
+
+setEntityMeshAsync(
+    entityId: entity,
+    filename: "robot",
+    withExtension: "untold"
+) { success in
+    guard success else { return }
+    translateTo(entityId: entity, position: simd_float3(0, 0, 0))
+}
+
+

The completion Bool is a success flag:

+
    +
  • true: the asset loaded and registered successfully
  • +
  • false: loading failed and the engine fell back to the default placeholder mesh
  • +
+

It does not indicate whether the asset used an out-of-core path.

+

Scene Readiness Guard

+

Use scene readiness when your own setup performs multiple dependent mutations:

+
setSceneReady(false)
+
+let entity = createEntity()
+setEntityMeshAsync(entityId: entity, filename: "robot", withExtension: "untold") { success in
+    if success {
+        setEntityKinetics(entityId: entity)
+        translateTo(entityId: entity, position: simd_float3(0, 0, 0))
+    }
+    setSceneReady(success)
+}
+
+

For ordinary setEntityMeshAsync(...) and setEntityStreamScene(...) flows, the engine already uses internal loading gates. setSceneReady(...) is mainly for your own multi-step scene setup.

+

Choosing the Right Loading Path

+ + + + + + + + + + + + + + + + + +
Use caseAPI
Single always-resident assetsetEntityMeshAsync(...)
Large streamed worldsetEntityStreamScene(...)
+

Always-resident asset

+

Use setEntityMeshAsync for props, characters, gameplay objects, HUD meshes, and any asset that should stay resident.

+
setEntityMeshAsync(
+    entityId: entity,
+    filename: "stadium",
+    withExtension: "untold"
+) { success in
+    if success {
+        setEntityStaticBatchComponent(entityId: entity)
+    }
+}
+
+

Streamed world

+

Use setEntityStreamScene for geometry that should stream by camera distance:

+
let sceneRoot = createEntity()
+setEntityName(entityId: sceneRoot, name: "dungeon")
+
+setEntityStreamScene(entityId: sceneRoot, manifest: "dungeon", withExtension: "json") { success in
+    setSceneReady(success)
+}
+
+

Or load a remote manifest directly:

+
let sceneRoot = createEntity()
+setEntityName(entityId: sceneRoot, name: "dungeon")
+
+if let url = URL(string: "https://cdn.example.com/dungeon/dungeon.json") {
+    setEntityStreamScene(entityId: sceneRoot, url: url) { success in
+        setSceneReady(success)
+    }
+}
+
+
+

Legacy overloadsloadTiledScene(manifest:) and loadTiledScene(url:) remain available for backwards compatibility.

+
+

This is the public streaming workflow. Do not build app-level streaming logic around StreamingComponent or enableStreaming(...); those are internal to the tile/OCC pipeline.

+

streamingPolicy

+

setEntityMeshAsync accepts a streamingPolicy parameter to control how geometry +is uploaded to the GPU. For standalone assets, two values are relevant:

+
    +
  • .auto — default; the engine chooses full upload or incremental based on asset size
  • +
  • .immediate — always uploads in a single pass; use for props that must appear fully + formed on first frame (player characters, weapons, HUD objects)
  • +
+
setEntityMeshAsync(
+    entityId: entity,
+    filename: "small_prop",
+    withExtension: "untold",
+    streamingPolicy: .immediate
+)
+
+

.outOfCore is reserved for the engine's internal tile streaming pipeline. Passing it +directly on a standalone entity is unsupported — StreamingComponent stubs created +outside of a TileComponent hierarchy are not managed by GeometryStreamingSystem. +Use setEntityStreamScene(...) instead when you need distance-based streaming.

+

Progress Tracking

+

Is anything loading?

+
Task {
+    let isLoading = await AssetLoadingState.shared.isLoadingAny()
+    print("Loading: \(isLoading)")
+}
+
+

Loading count

+
Task {
+    let count = await AssetLoadingState.shared.loadingCount()
+    print("Loading \(count) asset(s)")
+}
+
+

Aggregate progress

+
Task {
+    let (current, total) = await AssetLoadingState.shared.totalProgress()
+    let percentage = total > 0 ? Float(current) / Float(total) * 100.0 : 0.0
+    print("Progress: \(percentage)% (\(current)/\(total))")
+}
+
+

Human-readable summary

+
Task {
+    let summary = await AssetLoadingState.shared.loadingSummary()
+    print(summary)
+}
+
+

Per-entity progress

+
Task {
+    if let progress = await AssetLoadingState.shared.getProgress(for: entity) {
+        print("\(progress.filename): \(progress.currentMesh)/\(progress.totalMeshes)")
+    }
+}
+
+

Notes

+
    +
  • .untold is the preferred runtime format for static geometry.
  • +
  • Animation clips exported with --animation can be loaded as .untold assets.
  • +
  • setEntityStreamScene(...) automatically aligns texture streaming distances to the manifest radii and enables the full tile/HLOD/LOD/OCC streaming pipeline.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingCameraSystem/index.html b/site/API/UsingCameraSystem/index.html new file mode 100644 index 00000000..0c546031 --- /dev/null +++ b/site/API/UsingCameraSystem/index.html @@ -0,0 +1,2313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Camera System - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Using the Camera System

+

This document explains how to move, rotate, and control cameras using the APIs in CameraSystem.swift.

+

Get the Game Camera

+

For gameplay, always use the game camera (not the editor/scene camera). Call findGameCamera() and make it active:

+
let camera = findGameCamera()
+CameraSystem.shared.activeCamera = camera
+
+

If no game camera exists, findGameCamera() creates one and sets it up with default values.

+

Translate (Move) the Camera

+

Use absolute or relative movement:

+
// Absolute position
+moveCameraTo(entityId: camera, 0.0, 3.0, 7.0)
+
+// Relative movement in camera local space
+cameraMoveBy(entityId: camera, delta: simd_float3(0.0, 0.0, -1.0), space: .local)
+
+// Relative movement in world space
+cameraMoveBy(entityId: camera, delta: simd_float3(1.0, 0.0, 0.0), space: .world)
+
+

Rotate the Camera

+

Use rotateCamera for pitch/yaw rotation, or cameraLookAt to aim at a target.

+
// Rotate by pitch/yaw (radians), with optional sensitivity
+rotateCamera(entityId: camera, pitch: 0.02, yaw: 0.01, sensitivity: 1.0)
+
+// Look-at orientation
+cameraLookAt(
+    entityId: camera,
+    eye: simd_float3(0.0, 3.0, 7.0),
+    target: simd_float3(0.0, 0.0, 0.0),
+    up: simd_float3(0.0, 1.0, 0.0)
+)
+
+

Camera Follow

+

Follow a target entity with a fixed offset. You can optionally smooth the motion.

+
let target = findEntity(name: "player") ?? createEntity()
+let offset = simd_float3(0.0, 2.0, 6.0)
+
+// Instant follow
+cameraFollow(entityId: camera, targetEntity: target, offset: offset)
+
+// Smoothed follow
+cameraFollow(entityId: camera, targetEntity: target, offset: offset, smoothFactor: 6.0, deltaTime: deltaTime)
+
+

Dead-Zone Follow

+

cameraFollowDeadZone only moves the camera when the target leaves a box around it. This is useful for platformers and shoulder cameras.

+
let deadZone = simd_float3(1.0, 0.5, 1.0)
+cameraFollowDeadZone(
+    entityId: camera,
+    targetEntity: target,
+    offset: offset,
+    deadZoneExtents: deadZone,
+    smoothFactor: 6.0,
+    deltaTime: deltaTime
+)
+
+

Camera Path Following

+

The camera path system moves the active camera through a sequence of waypoints with smooth interpolation.

+

Start a Path

+
let waypoints = [
+    CameraWaypoint(
+        position: simd_float3(0, 5, 10),
+        rotation: simd_quatf(angle: 0, axis: simd_float3(0, 1, 0)),
+        segmentDuration: 2.0
+    ),
+    CameraWaypoint(
+        position: simd_float3(10, 5, 10),
+        rotation: simd_quatf(angle: Float.pi / 4, axis: simd_float3(0, 1, 0)),
+        segmentDuration: 2.0
+    )
+]
+
+startCameraPath(waypoints: waypoints, mode: .once)
+
+

You can also build waypoints that look at a target:

+
let waypoint = CameraWaypoint(
+    position: simd_float3(0, 5, 10),
+    lookAt: simd_float3(0, 0, 0),
+    up: simd_float3(0, 1, 0),
+    segmentDuration: 2.0
+)
+
+

Update Every Frame

+

Call updateCameraPath(deltaTime:) from your main update loop:

+
func update(deltaTime: Float) {
+    updateCameraPath(deltaTime: deltaTime)
+}
+
+

Looping and Completion

+
startCameraPath(waypoints: waypoints, mode: .loop)
+
+let settings = CameraPathSettings(startImmediately: true) {
+    print("Camera path completed")
+}
+startCameraPath(waypoints: waypoints, mode: .once, settings: settings)
+
+

Notes

+
    +
  • startCameraPath and updateCameraPath operate on CameraSystem.shared.activeCamera.
  • +
  • segmentDuration is the time to move from the current waypoint to the next.
  • +
  • For gameplay, always acquire the camera with findGameCamera() and set it active before path playback or follow logic.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingGaussianSystem/index.html b/site/API/UsingGaussianSystem/index.html new file mode 100644 index 00000000..1922c296 --- /dev/null +++ b/site/API/UsingGaussianSystem/index.html @@ -0,0 +1,2130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Gaussian System - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Enabling Gaussian System in Untold Engine

+

The Gaussian System in the Untold Engine is responsible for rendering Gaussian Splatting models. It enables you to visualize high-quality 3D reconstructions created from photogrammetry or neural rendering techniques, providing a modern approach to displaying complex 3D scenes.

+

How to Enable the Gaussian System

+

Step 1: Create an Entity

+

Start by creating an entity that represents your Gaussian Splat object.

+

let myEntity = createEntity()
+

+ +

To display a Gaussian Splat model, load its .ply file and link it to the entity using setEntityGaussian.

+
setEntityGaussian(entityId: myEntity, filename: "splat", withExtension: "ply")
+
+

Parameters:

+
    +
  • entityId: The ID of the entity created earlier.
  • +
  • filename: The name of the .ply file (without the extension).
  • +
  • withExtension: The file extension, typically "ply".
  • +
+
+

Note: The Gaussian System renders point cloud data stored in the .ply format. Ensure your Gaussian Splat file is properly formatted and contains the necessary attributes (position, color, opacity, scale, rotation).

+
+
+

Running the Gaussian System

+

Once everything is set up:

+
    +
  1. Run the project.
  2. +
  3. Your Gaussian Splat model will appear in the game window.
  4. +
  5. If the model is not visible or appears incorrect, revisit the file path and format to ensure everything is loaded correctly.
  6. +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingGeometryStreamingSystem/index.html b/site/API/UsingGeometryStreamingSystem/index.html new file mode 100644 index 00000000..67feebfb --- /dev/null +++ b/site/API/UsingGeometryStreamingSystem/index.html @@ -0,0 +1,2463 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Geometry Streaming - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Geometry Streaming System

+

UntoldEngine streams large worlds through a manifest-driven tiled scene pipeline.

+

The public rule is simple:

+ + + + + + + + + + + + + + + + + + + + + +
Use caseAPI
Streamed world geometry (manifest-driven)setEntityStreamScene(entityId:manifest:withExtension:completion:)
Handcrafted streaming zones (no manifest)StreamingRegionManager — register StreamingRegion AABB + asset lists directly
Always-resident assetssetEntityMeshAsync(entityId:filename:withExtension:completion:)
+

GeometryStreamingSystem manages the runtime once a streamed scene is loaded. It is not a public component-authoring workflow for standalone entities.

+
+

For handcrafted zone streaming without a manifest (e.g. dungeon rooms, level sectors), use StreamingRegionManager.shared. See the StreamingRegionManager architecture doc for the full API.

+
+

Public Workflow

+

Local manifest

+
let sceneRoot = createEntity()
+setEntityName(entityId: sceneRoot, name: "city")
+
+setEntityStreamScene(entityId: sceneRoot, manifest: "city", withExtension: "json") { success in
+    setSceneReady(success)
+}
+
+

Remote manifest

+
let sceneRoot = createEntity()
+setEntityName(entityId: sceneRoot, name: "city")
+
+if let url = URL(string: "https://cdn.example.com/city/city.json") {
+    setEntityStreamScene(entityId: sceneRoot, url: url) { success in
+        setSceneReady(success)
+    }
+}
+
+
+

Legacy overloadsloadTiledScene(manifest:) and loadTiledScene(url:) remain available for backwards compatibility. They create an internal root entity automatically. Prefer setEntityStreamScene(entityId:...) when you need a stable handle to the scene.

+
+

Remote manifests are downloaded and cached locally. Tile, HLOD, and per-tile LOD URLs are resolved relative to the manifest URL and fetched on demand.

+

What Streams

+

The engine uses multiple geometry layers:

+
    +
  • Full tile: the main tile payload loaded by loadTile()
  • +
  • Per-tile LOD: intermediate meshes shown while the full tile is still out of range
  • +
  • HLOD: coarse far-distance proxy
  • +
  • OCC sub-mesh stubs: fine-grained StreamingComponent entities created internally inside large tiles
  • +
+

StreamingComponent is internal to the tile-owned OCC path. External callers should not attach it manually or rely on enableStreaming(...).

+

Manifest Fields That Matter

+

These are the important fields for geometry streaming:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldMeaning
streaming_radiusFull tile display zone
unload_radiusTile teardown threshold
prefetch_radiusBackground parse threshold before the tile becomes visible
priorityTile load ordering when many tiles compete
hlod_levelsOptional far proxy meshes
lod_levelsOptional per-tile intermediate LOD meshes
file_size_bytesParse-budget hint used by the runtime gate
+

If prefetch_radius is omitted, the engine computes it automatically from the gap between streaming_radius and unload_radius.

+

Runtime Behavior

+

Each update tick, GeometryStreamingSystem:

+
    +
  1. Queries the octree within maxQueryRadius.
  2. +
  3. Chooses tile parse candidates using predictive camera motion and a frustum gate.
  4. +
  5. Parses up to maxConcurrentTileLoads tiles, subject to tileParseMemoryBudgetMB.
  6. +
  7. Streams OCC child meshes inside loaded tiles using maxConcurrentLoads.
  8. +
  9. Unloads tiles, LODs, HLODs, and OCC meshes when they leave range or memory pressure requires eviction.
  10. +
+

Important defaults:

+
    +
  • maxConcurrentTileLoads = 2
  • +
  • maxConcurrentLoads = 3
  • +
  • maxConcurrentLODLoads = 4
  • +
  • maxConcurrentHLODLoads = 4
  • +
  • updateInterval = 0.1
  • +
  • burstTickInterval = 0.016
  • +
+

Useful Runtime Knobs

+
GeometryStreamingSystem.shared.maxConcurrentTileLoads = 2
+GeometryStreamingSystem.shared.maxConcurrentLoads = 3
+GeometryStreamingSystem.shared.enableFrustumGate = true
+GeometryStreamingSystem.shared.tileFrustumGatePadding = 20.0
+GeometryStreamingSystem.shared.maxQueryRadius = 500.0
+
+

Use maxQueryRadius large enough to cover the farthest unload_radius in the scene, or out-of-range tiles may not be discovered for teardown.

+

Interaction with Other Systems

+
    +
  • Texture streaming: setEntityStreamScene(...) automatically aligns texture distance bands to the manifest radii.
  • +
  • Batching: full-load tiles, per-tile LODs, and HLODs notify BatchingSystem automatically. OCC sub-mesh uploads join batching incrementally through normal residency events.
  • +
  • Memory pressure: texture quality is shed first; geometry eviction follows only when geometry pressure remains high.
  • +
+

Common Problems

+

Tiles pop in on camera rotation

+
    +
  • Increase GeometryStreamingSystem.shared.tileFrustumGatePadding
  • +
  • Keep enableFrustumGate = true
  • +
+

Tiles unload and reload too aggressively

+
    +
  • Increase the gap between streaming_radius and unload_radius
  • +
  • Increase or explicitly author prefetch_radius
  • +
+

Tile parse bursts spike memory

+
    +
  • Lower maxConcurrentTileLoads
  • +
  • Reduce per-tile file sizes in the exported manifest
  • +
+

Streaming does nothing

+
    +
  • Verify you loaded the scene through setEntityStreamScene(...)
  • +
  • Verify the manifest radii are reasonable for your scene scale
  • +
  • Do not expect standalone StreamingComponent entities to stream; tile ownership is enforced
  • +
+ + + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingInputSystem/index.html b/site/API/UsingInputSystem/index.html new file mode 100644 index 00000000..6a9c1c31 --- /dev/null +++ b/site/API/UsingInputSystem/index.html @@ -0,0 +1,2195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Input System - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Using the Input System in Untold Engine

+

The Input System in the Untold Engine allows you to detect user inputs, such as keystrokes and mouse movements, to control entities and interact with the game. This guide will explain how to use the Input System effectively.

+

How to Use the Input System (Keyboard)

+

Step 1: Detect Keystrokes

+

To detect if a specific key is pressed, use the keyState object from the Input System.

+

Example: Detecting the 'W' Key

+

func init(){
+// Make sure that you have enabled keyevents in your init function:
+InputSystem.shared.registerKeyboardEvents()
+}
+
+// Then in the handleInput callback, you can do this:
+
+func handleInput() {
+    // Skip logic if not in game mode
+    if gameMode == false { return }
+
+    let inputSystem = InputSystem.shared
+
+    // Handle input here
+    if inputSystem.keyState.wPressed{
+        Logger.log(message: "w pressed")
+    }
+}
+
+You can use the same logic for other keys like A, S, and D:

+
let inputSystem = InputSystem.shared
+
+if inputSystem.keyState.aPressed == true {
+    // Move left
+}
+
+if inputSystem.keyState.sPressed == true {
+    // Move backward
+}
+
+if inputSystem.keyState.dPressed == true {
+    // Move right
+}
+
+

Step 2: Using Input to Control Entities

+

Here’s an example function that moves a car entity based on keyboard inputs:

+
func moveCar(entityId: EntityID, dt: Float) {
+
+    let inputSystem = InputSystem.shared
+
+    // Ensure we are in game mode
+    if gameMode == false {
+        return
+    }
+
+    var position = simd_float3(0.0, 0.0, 0.0)
+
+    // Move forward
+    if inputSystem.keyState.wPressed == true {
+        position.z += 1.0 * dt
+    }
+
+    // Move backward
+    if inputSystem.keyState.sPressed == true {
+        position.z -= 1.0 * dt
+    }
+
+    // Move left
+    if inputSystem.keyState.aPressed == true {
+        position.x -= 1.0 * dt
+    }
+
+    // Move right
+    if inputSystem.keyState.dPressed == true {
+        position.x += 1.0 * dt
+    }
+
+    // Apply the translation to the entity
+    translateTo(entityId: entityId, position: position)
+}
+
+

How to Use the Input System with a Game Controller

+

To detect if a specific button is pressed, use the gameControllerState object from the Input System.

+

Example: Detecting the 'A' button

+
func init(){
+// Make sure that you have enabled game controller events in your init function:
+    InputSystem.shared.registerGameControllerEvents()
+}
+
+// Then in the handleInput callback, you can do this:
+
+func handleInput() {
+    // Skip logic if not in game mode
+    if gameMode == false { return }
+    let inputSystem = InputSystem.shared
+
+    // Handle input here
+    if inputSystem.gameControllerState.aPressed {
+        Logger.log(message: "Pressed A key")
+    }
+}
+
+
+

Tips and Best Practices

+
    +
  • Debouncing: If you want to execute an action only once per key press, track the key's previous state to avoid repeated triggers.
  • +
  • Game Mode Check: Always ensure the game is in the appropriate mode (e.g., Game Mode) before processing inputs.
  • +
  • Smooth Movement: Use dt (delta time) to ensure frame-rate-independent movement.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingLOD-Batching-Streaming/index.html b/site/API/UsingLOD-Batching-Streaming/index.html new file mode 100644 index 00000000..63f99750 --- /dev/null +++ b/site/API/UsingLOD-Batching-Streaming/index.html @@ -0,0 +1,2233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + LOD + Batching + Streaming - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Combining LOD, Batching, and Streaming

+

UntoldEngine now has two distinct optimization workflows:

+ + + + + + + + + + + + + + + + + +
WorkflowUse for
Entity-level LOD + manual batchingAlways-resident props, structures, authored gameplay objects
Manifest-driven tile streaming + automatic batchingLarge worlds, terrain, cities, remote streamed scenes
+

1. Always-Resident Objects

+

For normal entities that should stay resident, combine LODComponent with StaticBatchComponent:

+
private func setupLODWithBatching() {
+    var loadedCount = 0
+    let totalTrees = 20
+
+    for i in 0 ..< totalTrees {
+        let tree = createEntity()
+        setEntityName(entityId: tree, name: "Tree_\(i)")
+        setEntityLodComponent(entityId: tree)
+
+        let x = Float(i % 5) * 10.0
+        let z = Float(i / 5) * 10.0
+
+        addLODLevels(entityId: tree, levels: [
+            (0, "tree_LOD0", "untold", 50.0, 0.0),
+            (1, "tree_LOD1", "untold", 100.0, 0.0),
+            (2, "tree_LOD2", "untold", 200.0, 0.0),
+        ]) { success in
+            if success {
+                translateTo(entityId: tree, position: simd_float3(x, 0, z))
+                setEntityStaticBatchComponent(entityId: tree)
+            }
+
+            loadedCount += 1
+            if loadedCount == totalTrees {
+                enableBatching(true)
+                generateBatches()
+            }
+        }
+    }
+}
+
+

This is still the correct pattern when all meshes are present up front and stay resident.

+

2. Streamed Worlds

+

For large worlds, do not build a manual LOD + enableStreaming(...) stack on standalone entities. Use the tiled-scene pipeline:

+
let sceneRoot = createEntity()
+setEntityName(entityId: sceneRoot, name: "city")
+
+setEntityStreamScene(entityId: sceneRoot, manifest: "city", withExtension: "json") { success in
+    setSceneReady(success)
+}
+
+

In this workflow:

+
    +
  • full-tile streaming is driven by manifest radii
  • +
  • per-tile lod_levels supply intermediate representations
  • +
  • hlod_levels cover the far field
  • +
  • OCC mesh stubs are created internally for large tiles
  • +
  • static batching is updated automatically as tiles, LODs, and OCC meshes become resident
  • +
+

You do not call generateBatches() per tile. The runtime hands new resident tile geometry directly to BatchingSystem.

+

Choosing Between the Two

+

Use entity-level LOD + batching when:

+
    +
  • the asset should remain in memory
  • +
  • you are placing a bounded number of authored objects
  • +
  • you want direct programmatic control over LOD levels per entity
  • +
+

Use tile streaming when:

+
    +
  • the scene is large enough that full residency is wasteful
  • +
  • you need prefetch, HLOD, per-tile LOD, and eviction
  • +
  • you want local or remote manifest-driven world streaming
  • +
+

Practical Rules

+
    +
  • Keep dynamic or animated entities out of static batching.
  • +
  • Use .untold for static runtime geometry whenever possible.
  • +
  • Keep entity-level LOD for authored objects; keep tile LOD/HLOD in the manifest.
  • +
  • Treat StreamingComponent as internal to the tiled streaming architecture.
  • +
+ + + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingLODSystem/index.html b/site/API/UsingLODSystem/index.html new file mode 100644 index 00000000..1731ee05 --- /dev/null +++ b/site/API/UsingLODSystem/index.html @@ -0,0 +1,3273 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + LOD System - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + + + + + + + +

LOD (Level of Detail) System - Usage Guide

+

The Untold Engine provides a flexible LOD system for optimizing rendering performance by displaying different mesh details based on camera distance.

+

Overview

+

The LOD system allows you to: +- Add multiple levels of detail to any entity +- Automatically switch between LOD levels based on distance +- Customize distance thresholds for each LOD level +- Configure LOD behavior (bias, hysteresis, fade transitions)

+
+

Choose Your Path: You can set up LOD via the Editor (no code required) or programmatically in Swift.

+
+
+

Using the Editor

+

Adding LOD Support to an Entity

+
    +
  1. Select your entity in the Scene Hierarchy
  2. +
  3. Open the Inspector and click "Add Components"
  4. +
  5. Select "LOD Component" from the list
  6. +
  7. An LOD Levels panel will appear in the Inspector
  8. +
+

Adding LOD Levels

+
    +
  1. Select a model from the Asset Browser (Models folder)
  2. +
  3. In the LOD Levels panel, click "Add LOD Level"
  4. +
  5. The selected model will be added as the next LOD level with a default distance:
  6. +
  7. LOD0: 50 units
  8. +
  9. LOD1: 100 units
  10. +
  11. LOD2: 150 units, etc.
  12. +
+

Adjusting LOD Distances

+
    +
  1. Click the distance value next to any LOD level
  2. +
  3. Enter a new distance and press Enter
  4. +
  5. Objects will switch to this LOD when the camera is within this distance
  6. +
+

Removing LOD Levels

+

Click the trash icon next to any LOD level to remove it.

+
+

Using Code

+

Quick Start

+

Basic LOD Setup

+
// Create entity
+let tree = createEntity()
+
+// Add LOD component
+setEntityLodComponent(entityId: tree)
+
+// Add LOD levels (from highest to lowest detail)
+addLODLevel(entityId: tree, lodIndex: 0, fileName: "tree_LOD0", withExtension: "untold", maxDistance: 50.0)
+addLODLevel(entityId: tree, lodIndex: 1, fileName: "tree_LOD1", withExtension: "untold", maxDistance: 100.0)
+addLODLevel(entityId: tree, lodIndex: 2, fileName: "tree_LOD2", withExtension: "untold", maxDistance: 200.0)
+addLODLevel(entityId: tree, lodIndex: 3, fileName: "tree_LOD3", withExtension: "untold", maxDistance: 400.0)
+
+

How it works: +- LOD0 (highest detail) renders when camera is < 50 units away +- LOD1 renders between 50-100 units +- LOD2 renders between 100-200 units +- LOD3 (lowest detail) renders beyond 200 units

+ +

Use addLODLevels to load all LOD levels with a single completion handler. This is especially important when combining LOD with static batching:

+
let tree = createEntity()
+setEntityLodComponent(entityId: tree)
+
+// Load all LOD levels and wait for completion
+addLODLevels(entityId: tree, levels: [
+    (0, "tree_LOD0", "untold", 50.0, 0.0),
+    (1, "tree_LOD1", "untold", 100.0, 0.0),
+    (2, "tree_LOD2", "untold", 200.0, 0.0),
+    (3, "tree_LOD3", "untold", 400.0, 0.0)
+]) { success in
+    if success {
+        print("All LOD levels loaded")
+
+        // Apply transforms AFTER mesh is loaded
+        translateTo(entityId: tree, position: simd_float3(10, 0, 5))
+
+        // Apply static batching if necessary
+    }
+}
+
+
+

Important: When using LOD with async loading, apply transforms (translateTo, rotateTo, scaleTo) inside the completion handler. Transforms applied before the mesh loads may not take effect.

+
+

With Initial Mesh Loading

+

You can also load an initial mesh synchronously before adding LOD levels:

+
let tree = createEntity()
+
+// Load initial mesh synchronously (shows immediately)
+setEntityMesh(entityId: tree, filename: "tree_LOD0", withExtension: "untold")
+
+// Add LOD component
+setEntityLodComponent(entityId: tree)
+
+// Add LOD levels (will replace initial mesh when ready)
+addLODLevel(entityId: tree, lodIndex: 0, fileName: "tree_LOD0", withExtension: "untold", maxDistance: 50.0)
+addLODLevel(entityId: tree, lodIndex: 1, fileName: "tree_LOD1", withExtension: "untold", maxDistance: 100.0)
+addLODLevel(entityId: tree, lodIndex: 2, fileName: "tree_LOD2", withExtension: "untold", maxDistance: 200.0)
+addLODLevel(entityId: tree, lodIndex: 3, fileName: "tree_LOD3", withExtension: "untold", maxDistance: 400.0)
+
+

With Completion Handler

+

Since addLODLevel loads meshes asynchronously, use the completion handler when you need to perform actions after loading completes:

+
let tree = createEntity()
+setEntityLodComponent(entityId: tree)
+
+// Chain completion handlers for sequential loading
+addLODLevel(entityId: tree, lodIndex: 0, fileName: "tree_LOD0", withExtension: "untold", maxDistance: 50.0) { success in
+    if success {
+        print("LOD0 loaded")
+        // Now it's safe to use the mesh data
+    }
+}
+
+

File Organization

+

LOD files should be organized in subdirectories:

+
GameData/
+└── Models/
+    ├── tree_LOD0/
+    │   └── tree_LOD0.untold
+    ├── tree_LOD1/
+    │   └── tree_LOD1.untold
+    ├── tree_LOD2/
+    │   └── tree_LOD2.untold
+    └── tree_LOD3/
+        └── tree_LOD3.untold
+
+

Note: Each LOD file should be in its own folder with the same name as the file (without extension).

+

API Reference

+

Core Functions

+

setEntityLodComponent(entityId:)

+

Registers an LOD component on an entity. Call this before adding LOD levels.

+
setEntityLodComponent(entityId: tree)
+
+

addLODLevel(entityId:lodIndex:fileName:withExtension:maxDistance:completion:)

+

Adds a single LOD level to an entity.

+

Parameters: +- entityId: The entity to add LOD to +- lodIndex: LOD level index (0 = highest detail) +- fileName: Name of the mesh file (without extension) +- withExtension: File extension (e.g., "untold") +- maxDistance: Maximum camera distance for this LOD +- completion: Optional callback when loading completes

+
addLODLevel(
+    entityId: tree,
+    lodIndex: 0,
+    fileName: "tree_LOD0",
+    withExtension: "untold",
+    maxDistance: 50.0
+) { success in
+    if success {
+        print("LOD0 loaded successfully")
+    }
+}
+
+

addLODLevels(entityId:levels:completion:)

+

Adds multiple LOD levels with a single completion handler. Useful when you need to wait for all LOD levels to load.

+

Parameters: +- entityId: The entity to add LOD levels to +- levels: Array of tuples: (lodIndex, fileName, withExtension, maxDistance, screenPercentage) +- completion: Called when ALL levels finish loading (true only if all succeeded)

+
addLODLevels(entityId: tree, levels: [
+    (0, "tree_LOD0", "untold", 50.0, 0.0),
+    (1, "tree_LOD1", "untold", 100.0, 0.0),
+    (2, "tree_LOD2", "untold", 200.0, 0.0)
+]) { success in
+    if success {
+        print("All LODs loaded")
+    }
+}
+
+

removeLODLevel(entityId:lodIndex:)

+

Removes a specific LOD level from an entity.

+
removeLODLevel(entityId: tree, lodIndex: 2)
+
+

replaceLODLevel(entityId:lodIndex:fileName:withExtension:maxDistance:completion:)

+

Replaces an existing LOD level with a new mesh.

+
replaceLODLevel(
+    entityId: tree,
+    lodIndex: 1,
+    fileName: "tree_LOD1_new",
+    withExtension: "untold",
+    maxDistance: 100.0
+)
+
+

getLODLevelCount(entityId:) -> Int

+

Returns the number of LOD levels for an entity.

+
let count = getLODLevelCount(entityId: tree)
+print("Entity has \(count) LOD levels")
+
+
+

Advanced Usage

+

Custom Distance Thresholds

+

Adjust distances based on your scene scale:

+
// Small scene (indoor environment)
+addLODLevel(entityId: prop, lodIndex: 0, fileName: "prop_LOD0", withExtension: "untold", maxDistance: 10.0)
+addLODLevel(entityId: prop, lodIndex: 1, fileName: "prop_LOD1", withExtension: "untold", maxDistance: 20.0)
+
+// Large scene (outdoor landscape)
+addLODLevel(entityId: mountain, lodIndex: 0, fileName: "mountain_LOD0", withExtension: "untold", maxDistance: 500.0)
+addLODLevel(entityId: mountain, lodIndex: 1, fileName: "mountain_LOD1", withExtension: "untold", maxDistance: 1000.0)
+addLODLevel(entityId: mountain, lodIndex: 2, fileName: "mountain_LOD2", withExtension: "untold", maxDistance: 2000.0)
+
+

LOD Configuration

+

Configure global LOD behavior:

+
// Adjust LOD bias (higher = switch to lower detail sooner)
+LODConfig.shared.lodBias = 1.5  // Performance mode
+LODConfig.shared.lodBias = 0.75 // Quality mode
+
+// Adjust hysteresis to prevent flickering
+LODConfig.shared.hysteresis = 10.0
+
+// Enable fade transitions between LODs - Not yet implemented
+LODConfig.shared.enableFadeTransitions = true
+LODConfig.shared.fadeTransitionTime = 0.5 // seconds
+
+

Forced LOD Override

+

Force a specific LOD level (useful for debugging):

+
if let lodComponent = scene.get(component: LODComponent.self, for: tree) {
+    lodComponent.forcedLOD = 2  // Always show LOD2
+    // lodComponent.forcedLOD = nil  // Resume automatic LOD selection
+}
+
+

Programmatic LOD Management

+
// Create entity with LOD component
+let rock = createEntity()
+setEntityLodComponent(entityId: rock)
+
+// Add LODs dynamically based on performance
+let lodFiles = ["rock_LOD0", "rock_LOD1", "rock_LOD2"]
+let distances: [Float] = [30.0, 60.0, 120.0]
+
+for (index, fileName) in lodFiles.enumerated() {
+    addLODLevel(
+        entityId: rock,
+        lodIndex: index,
+        fileName: fileName,
+        withExtension: "untold",
+        maxDistance: distances[index]
+    )
+}
+
+// Check LOD count
+let lodCount = getLODLevelCount(entityId: rock)
+print("Rock has \(lodCount) LOD levels")
+
+// Remove highest detail LOD on low-end hardware
+if isLowEndDevice {
+    removeLODLevel(entityId: rock, lodIndex: 0)
+}
+
+
+

Best Practices

+ +
    +
  • Small props: 2-3 LODs
  • +
  • Characters: 3-4 LODs
  • +
  • Vehicles: 3-4 LODs
  • +
  • Buildings: 4-5 LODs
  • +
  • Terrain: 5-8 LODs
  • +
+

Polygon Reduction Guidelines

+
    +
  • LOD0 (full detail): 100% polygons
  • +
  • LOD1: ~50% polygon reduction
  • +
  • LOD2: ~75% polygon reduction
  • +
  • LOD3: ~90% polygon reduction or billboard
  • +
+

Distance Thresholds

+

Base distances on object importance and size: +- Hero objects: Longer high-detail distance +- Background objects: Shorter high-detail distance +- Large objects: Visible from farther away, need more LODs

+

Performance Tips

+
    +
  1. Always use async loading (setEntityMeshAsync) for better performance
  2. +
  3. Keep LOD0 for objects within 50 units of camera
  4. +
  5. Use billboards or impostors for very distant objects (LOD3+)
  6. +
  7. Test LOD transitions in-game to ensure smooth visual quality
  8. +
  9. Use forcedLOD during development to preview each LOD level
  10. +
+

Troubleshooting

+

LODs Not Switching

+
    +
  • Verify LOD component is registered: hasComponent(entityId: tree, componentType: LODComponent.self)
  • +
  • Check distance thresholds are set correctly
  • +
  • Ensure camera has CameraComponent and is active
  • +
+

Visual Popping Between LODs

+
    +
  • Increase LODConfig.shared.hysteresis value
  • +
  • Enable fade transitions: LODConfig.shared.enableFadeTransitions = true - not yet implemented
  • +
  • Adjust LOD bias for smoother transitions
  • +
+

File Not Found Errors

+
    +
  • Verify file organization follows the subdirectory structure
  • +
  • Check file names match exactly (case-sensitive)
  • +
  • Ensure files are in the correct GameData/Models/ path
  • +
+

Performance Issues

+
    +
  • Reduce number of LOD levels for less important objects
  • +
  • Increase distance thresholds to switch LODs sooner
  • +
  • Use LOD bias > 1.0 for performance mode
  • +
+

Example: Complete LOD Setup

+
import UntoldEngine
+
+// Create multiple trees with LODs
+var trees: [EntityID] = []
+
+for i in 0..<10 {
+    let tree = createEntity()
+    setEntityName(entityId: tree, name: "Tree_\(i)")
+
+    // Position trees
+    translateTo(entityId: tree, position: simd_float3(Float(i * 10), 0, 0))
+
+    // Add LOD component
+    setEntityLodComponent(entityId: tree)
+
+    // Add 4 LOD levels
+    addLODLevel(entityId: tree, lodIndex: 0, fileName: "tree_LOD0", withExtension: "untold", maxDistance: 50.0)
+    addLODLevel(entityId: tree, lodIndex: 1, fileName: "tree_LOD1", withExtension: "untold", maxDistance: 100.0)
+    addLODLevel(entityId: tree, lodIndex: 2, fileName: "tree_LOD2", withExtension: "untold", maxDistance: 200.0)
+    addLODLevel(entityId: tree, lodIndex: 3, fileName: "tree_LOD3", withExtension: "untold", maxDistance: 400.0)
+
+    trees.append(tree)
+}
+
+// Configure LOD system for this scene
+LODConfig.shared.lodBias = 1.2  // Slightly favor performance
+LODConfig.shared.hysteresis = 8.0  // Prevent flickering
+
+print("Created \(trees.count) trees with LOD support")
+
+

Example: LOD with Static Batching

+

When combining LOD with static batching, ensure transforms and batching setup happen after meshes are loaded:

+
import UntoldEngine
+
+private func setupLODWithBatching() {
+    var loadedCount = 0
+    let totalTrees = 20
+
+    for i in 0..<totalTrees {
+        let tree = createEntity()
+        setEntityName(entityId: tree, name: "Tree_\(i)")
+
+        // Capture position for the closure
+        let x = Float(i % 5) * 10.0
+        let z = Float(i / 5) * 10.0
+
+        // Add LOD component BEFORE loading levels
+        setEntityLodComponent(entityId: tree)
+
+        // Load all LOD levels with completion handler
+        addLODLevels(entityId: tree, levels: [
+            (0, "tree_LOD0", "untold", 50.0, 0.0),
+            (1, "tree_LOD1", "untold", 100.0, 0.0),
+            (2, "tree_LOD2", "untold", 200.0, 0.0)
+        ]) { success in
+            if success {
+                // Apply transform AFTER mesh is loaded
+                translateTo(entityId: tree, position: simd_float3(x, 0, z))
+
+                // Mark for batching
+                setEntityStaticBatchComponent(entityId: tree)
+            }
+
+            // Track completion
+            loadedCount += 1
+            if loadedCount == totalTrees {
+                // All trees loaded - generate batches
+                enableBatching(true)
+                generateBatches()
+                print("\(totalTrees) trees configured with LOD + Batching")
+            }
+        }
+    }
+}
+
+

Key points: +1. Call setEntityLodComponent() before loading LOD levels +2. Apply transforms (translateTo) inside the completion handler +3. Call setEntityStaticBatchComponent() after mesh is loaded +4. Call generateBatches() only after all entities are ready

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingLightingSystem/index.html b/site/API/UsingLightingSystem/index.html new file mode 100644 index 00000000..53fd40ae --- /dev/null +++ b/site/API/UsingLightingSystem/index.html @@ -0,0 +1,2114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lighting System - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + + + + + + + +

Enabling the Lighting System in Untold Engine

+

The Lighting System lets you add illumination to your scenes using common real-time light types. Under the hood it wires up the required ECS components, provides an editor-friendly visual handle, and tags the light so the renderer can pick it up.

+
+

Creating Each Light Type

+

Directional Light

+

Use for sunlight or distant key lights. Orientation (rotation) defines its direction.

+
let sun = createEntity()
+createDirLight(entityId: sun)
+
+

Point Light

+

Omni light that radiates equally in all directions from a position.

+
let bulb = createEntity()
+createPointLight(entityId: bulb)
+
+

Spot Light

+

Cone-shaped light with a position and direction.

+
let spot = createEntity()
+createSpotLight(entityId: spot)
+
+

Area Light

+

Rect/area emitter used to mimic panels/windows; position and orientation matter.

+
let panel = createEntity()
+createAreaLight(entityId: panel)
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingMaterials/index.html b/site/API/UsingMaterials/index.html new file mode 100644 index 00000000..94a8cfda --- /dev/null +++ b/site/API/UsingMaterials/index.html @@ -0,0 +1,2669 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Materials - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Using Materials in Untold Engine

+

The engine uses a PBR (Physically Based Rendering) material model. Each entity's mesh can contain one or more submeshes, and each submesh holds its own Material. You can read and write individual material properties at runtime using the functions below.

+

All material functions accept optional meshIndex and submeshIndex parameters (both default to 0) so you can target a specific submesh when an entity contains more than one.

+
+

Note: Every update function automatically refreshes static batching for the affected entity, so you do not need to do this manually.

+
+
+

Base Color

+

The base color is stored as a simd_float4 (RGBA). The .w component doubles as the opacity channel.

+

Get Base Color

+
let color = getMaterialBaseColor(entityId: entity)
+// color.x = red, color.y = green, color.z = blue, color.w = alpha
+
+

Set Base Color via SwiftUI Color

+
updateMaterialColor(entityId: entity, color: .red)
+
+

This converts the SwiftUI Color to RGBA internally. If the alpha is below 1.0, the material automatically switches to .blend alpha mode.

+
+

Roughness

+

Controls how rough or smooth a surface appears. A value of 0.0 is perfectly smooth (mirror-like reflections) and 1.0 is fully rough (diffuse).

+

Get Roughness

+
let roughness = getMaterialRoughness(entityId: entity)
+
+

Set Roughness

+
updateMaterialRoughness(entityId: entity, roughness: 0.3)
+
+
+

When a roughness texture is present, the scalar value acts as a modulator (multiplied with the texture sample in the shader). If you remove the texture, the scalar value is used directly.

+
+
+

Metallic

+

Controls how metallic a surface appears. 0.0 is fully dielectric (plastic, wood, etc.) and 1.0 is fully metallic.

+

Get Metallic

+
let metallic = getMaterialMetallic(entityId: entity)
+
+

Set Metallic

+
updateMaterialMetallic(entityId: entity, metallic: 1.0)
+
+
+

Like roughness, the scalar value modulates the metallic texture when one is present.

+
+
+

Emissive

+

Controls self-illumination. The value is a simd_float3 (RGB) representing the emitted light color and intensity. A value of .zero means no emission.

+

Get Emissive

+
let emissive = getMaterialEmmissive(entityId: entity)
+
+

Set Emissive

+
updateMaterialEmmisive(entityId: entity, emmissive: simd_float3(1.0, 0.5, 0.0))
+
+
+

Spelling note: The API currently uses getMaterialEmmissive / updateMaterialEmmisive (with double-m). Use these exact names when calling the functions.

+
+
+

Alpha Mode

+

Determines how the renderer handles transparency for this material.

+

Available Modes (MaterialAlphaMode)

+
    +
  • .opaque — Fully opaque. Alpha channel is ignored.
  • +
  • .mask — Binary transparency. Pixels with alpha below the cutoff are discarded; the rest are fully opaque. Useful for foliage, fences, etc.
  • +
  • .blend — Smooth alpha blending. Pixels are composited based on their alpha value.
  • +
+

Get Alpha Mode

+
let mode = getMaterialAlphaMode(entityId: entity) // returns MaterialAlphaMode
+
+

Set Alpha Mode

+
updateMaterialAlphaMode(entityId: entity, mode: .blend)
+
+
+

Alpha Cutoff

+

Used only when the alpha mode is .mask. Pixels with alpha below this threshold are discarded. The value is clamped to 0.0 ... 1.0. Default is 0.5.

+

Get Alpha Cutoff

+
let cutoff = getMaterialAlphaCutoff(entityId: entity)
+
+

Set Alpha Cutoff

+
updateMaterialAlphaCutoff(entityId: entity, cutoff: 0.3)
+
+
+

Opacity

+

A convenience layer over the base color's alpha channel (.w). The value is clamped to 0.0 ... 1.0. Setting opacity below 1.0 automatically switches the alpha mode to .blend.

+

Get Opacity

+
let opacity = getMaterialOpacity(entityId: entity)
+
+

Set Opacity (all submeshes)

+
updateMaterialOpacity(entityId: entity, opacity: 0.5)
+
+

By default this applies to every submesh on the entity. To target a single submesh instead:

+
updateMaterialOpacity(entityId: entity, opacity: 0.5, applyToAllSubmeshes: false)
+
+

Or specify exact indices:

+
updateMaterialOpacity(entityId: entity, opacity: 0.5, meshIndex: 0, submeshIndex: 1)
+
+
+

Quick Reference

+
    +
  • getMaterialBaseColor(entityId:meshIndex:submeshIndex:)simd_float4
  • +
  • updateMaterialColor(entityId:color:meshIndex:submeshIndex:) — sets base color from SwiftUI Color
  • +
  • getMaterialRoughness(entityId:meshIndex:submeshIndex:)Float
  • +
  • updateMaterialRoughness(entityId:roughness:meshIndex:submeshIndex:)
  • +
  • getMaterialMetallic(entityId:meshIndex:submeshIndex:)Float
  • +
  • updateMaterialMetallic(entityId:metallic:meshIndex:submeshIndex:)
  • +
  • getMaterialEmmissive(entityId:meshIndex:submeshIndex:)simd_float3
  • +
  • updateMaterialEmmisive(entityId:emmissive:meshIndex:submeshIndex:)
  • +
  • getMaterialAlphaMode(entityId:meshIndex:submeshIndex:)MaterialAlphaMode
  • +
  • updateMaterialAlphaMode(entityId:mode:meshIndex:submeshIndex:)
  • +
  • getMaterialAlphaCutoff(entityId:meshIndex:submeshIndex:)Float
  • +
  • updateMaterialAlphaCutoff(entityId:cutoff:meshIndex:submeshIndex:)
  • +
  • getMaterialOpacity(entityId:meshIndex:submeshIndex:)Float
  • +
  • updateMaterialOpacity(entityId:opacity:applyToAllSubmeshes:)
  • +
  • updateMaterialOpacity(entityId:opacity:meshIndex:submeshIndex:)
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingPhysicsSystem/index.html b/site/API/UsingPhysicsSystem/index.html new file mode 100644 index 00000000..99213b0e --- /dev/null +++ b/site/API/UsingPhysicsSystem/index.html @@ -0,0 +1,2400 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Physics System - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Enabling Physics System in Untold Engine

+

The physics system in the Untold Engine enables realistic simulations such as gravity, forces, and dynamic interactions. While collision support is still under development, this guide will walk you through adding physics to your entities.

+

How to Enable Physics

+

Step 1: Create an Entity

+

Start by creating an entity that represents the object you want to add physics to.

+

let redPlayer = createEntity()
+

+ +

Next, load your model’s mesh file and link it to the entity. This step visually represents your entity in the scene.

+

setEntityMesh(entityId: redPlayer, filename: "redplayer", withExtension: "untold")
+

+

Step 3: Enable Physics on the Entity

+

Activate the physics simulation for your entity using the setEntityKinetics function. This function prepares the entity for movement and dynamic interaction.

+

setEntityKinetics(entityId: redPlayer)
+

+

Step 4: Configure Physics Properties

+

You can customize the entity’s physics behavior by defining its mass and gravity scale:

+
    +
  • Mass: Determines the force needed to move the object. Heavier objects require more force.
  • +
  • Gravity Scale: Controls how strongly gravity affects the entity (default is 0.0).
  • +
+

setMass(entityId: redPlayer, mass: 0.5)
+setGravityScale(entityId: redPlayer, gravityScale: 1.0)
+

+

Step 5: Apply Forces (Optional)

+

You can apply a custom force to the entity for dynamic movement. This is useful for simulating actions like jumps or pushes.

+
applyForce(entityId: redPlayer, force: simd_float3(0.0, 0.0, 5.0))
+
+
+

Note: Forces are applied per frame. To avoid unintended behavior, only apply forces when necessary.

+
+
+

Step 6: Use the Steering System

+

For advanced movement behaviors, leverage the Steering System to steer entities toward or away from targets. This system automatically calculates the required forces.

+

Example: Steering Toward a Position

+
steerTo(entityId: redPlayer, targetPosition: simd_float3(0.0, 0.0, 5.0), maxSpeed: 2.0, deltaTime: deltaTime)
+
+
+

Additional Steering Functions

+

The Steering System includes other useful behaviors, such as:

+
    +
  • steerAway()
  • +
  • steerPursuit()
  • +
  • followPath()
  • +
+

These functions simplify complex movement patterns, making them easy to implement.

+
+

What Happens Behind the Scenes?

+
    +
  1. Physics Simulation:
  2. +
  3. Entities with physics enabled are updated each frame to account for forces, gravity, and other dynamic factors.
  4. +
  5. Transformations are recalculated based on velocity, acceleration, and forces applied.
  6. +
  7. Realistic Motion:
  8. +
  9. The system ensures consistent, physics-based movement without manual updates to the transform.
  10. +
+
+

Running the Simulation

+

Once you've set up physics, run the project to see it in action:

+
    +
  1. Launch the project: Your model will appear in the game window.
  2. +
  3. Press "P" to enter Game Mode:
  4. +
  5. Gravity and forces will affect the entity.
  6. +
  7. If forces are applied, you’ll see dynamic motion in real time.
  8. +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingPostFX/index.html b/site/API/UsingPostFX/index.html new file mode 100644 index 00000000..af1fd576 --- /dev/null +++ b/site/API/UsingPostFX/index.html @@ -0,0 +1,2402 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Post Effects - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Using Post-Effects (PostFX) in Untold Engine

+

The engine provides a post-processing system through the PostFX namespace. You can enable and configure individual effects, or apply a full preset that sets a curated combination of color grading and SSAO values in a single call.

+
+

Applying a Preset

+

The simplest way to set up post-effects is to apply one of the built-in presets:

+
PostFX.apply(.cinematic)
+
+

That single call configures color grading and SSAO together. No scene wiring or callback setup is needed — it works from anywhere in your game code.

+

Built-in Presets

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PresetDescription
.neutralAll effects disabled, default values restored
.cinematicSlightly underexposed, desaturated, strong SSAO — moody interior feel
.highContrastBoosted exposure and saturation, punchy SSAO — vivid outdoor scenes
.softAOSubtle color grading, wide-radius soft ambient occlusion
.archvizBright and airy, clean slightly warm whites, precise SSAO for edge detail — architectural visualization
+

Switching Presets at Runtime

+

Presets can be swapped at any point during gameplay — for example when transitioning between areas:

+
// Entering a dark dungeon
+PostFX.apply(.cinematic)
+
+// Entering a bright outdoor area
+PostFX.apply(.highContrast)
+
+// Reset everything to defaults
+PostFX.apply(.neutral)
+
+
+

Custom Presets

+

If the built-in presets do not fit your scene, create a PostFXPreset directly and apply it:

+
let sunset = PostFXPreset(
+    name: "Sunset",
+    colorGrading: true,
+    exposure: 0.1,
+    saturation: 1.3,
+    temperature: 0.4
+)
+
+PostFX.apply(sunset)
+
+

All parameters have defaults (matching .neutral), so you only need to specify the values you want to change.

+

PostFXPreset Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
nameStringIdentifier for the preset
colorGradingBoolfalseEnables color grading pass
exposureFloat0.0EV adjustment (-2.0 to 2.0)
brightnessFloat0.0Additive brightness (-1.0 to 1.0)
contrastFloat1.0Contrast multiplier (0.5 to 2.0)
saturationFloat1.0Saturation multiplier (0.0 to 2.0)
temperatureFloat0.0Color temperature (-1.0 cool to +1.0 warm)
tintFloat0.0Green/magenta tint (-1.0 to 1.0)
ssaoBoolfalseEnables screen-space ambient occlusion
ssaoRadiusFloat0.5Sample radius in world units (0.1 to 2.0)
ssaoBiasFloat0.025Self-occlusion bias (0.01 to 0.1)
ssaoIntensityFloat0.0Final SSAO multiplier (0.5 to 2.0)
+
+

Enabling Individual Effects

+

For fine-grained control outside of presets, you can enable or disable individual effects:

+
PostFX.setEnabled(.colorGrading, true)
+PostFX.setEnabled(.vignette, true)
+PostFX.setEnabled(.chromaticAberration, false)
+
+

And read their current state:

+
let isActive = PostFX.isEnabled(.bloom)
+
+

Available Effects

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
EffectDescription
.colorGradingExposure, brightness, contrast, saturation, temperature, tint
.colorCorrectionLift/gamma/gain per-channel color correction
.bloomThresholdBright-pass filter for bloom
.bloomCompositeBloom blend pass
.vignetteScreen-edge darkening
.chromaticAberrationRGB channel fringing
.depthOfFieldFocus blur
+
+

Adjusting Individual Effect Parameters

+

Each effect exposes its parameters through a shared singleton. Import UntoldEngine and write directly:

+
// Color grading
+ColorGradingParams.shared.exposure    = -0.2
+ColorGradingParams.shared.contrast    = 1.15
+ColorGradingParams.shared.saturation  = 0.9
+ColorGradingParams.shared.temperature = -0.1
+
+// Bloom
+BloomThresholdParams.shared.threshold = 0.6
+BloomThresholdParams.shared.intensity = 0.8
+BloomThresholdParams.shared.enabled   = true
+
+// Vignette
+VignetteParams.shared.intensity = 0.5
+VignetteParams.shared.radius    = 0.8
+VignetteParams.shared.enabled   = true
+
+// SSAO
+SSAOParams.shared.radius    = 0.8
+SSAOParams.shared.intensity = 0.75
+SSAOParams.shared.enabled   = true
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingProfiler/index.html b/site/API/UsingProfiler/index.html new file mode 100644 index 00000000..1072826c --- /dev/null +++ b/site/API/UsingProfiler/index.html @@ -0,0 +1,2219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Profiler - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Profiler

+

UntoldEngine profiling has two layers that are meant to be used together:

+
    +
  1. Structured metrics (stable numbers for trends and regressions)
  2. +
  3. Category logs (on-demand narrative traces for deep debugging)
  4. +
+

Use structured metrics as the source of truth, then enable category logs only when you need extra context.

+

Quick Start

+

Enable the profiler at runtime:

+
enableEngineMetrics = true
+
+

Or via environment variable:

+
export UNTOLD_METRICS=1
+./YourApp
+
+

Enable periodic frame stats logging:

+
setEngineStatsLogging(
+    enabled: true,
+    profile: .compact,    // or .verbose
+    intervalSeconds: 1.0
+)
+
+

Read profiler snapshots programmatically:

+
let metrics = EngineProfiler.shared.snapshot()
+print("CPU mean: \(metrics.cpuFrame.meanMs) ms")
+print("GPU mean: \(metrics.gpuCommandBuffer.meanMs) ms")
+
+let frameStats = getEngineStatsSnapshot()
+print("Frame: \(frameStats.frameIndex)")
+print("Update: \(frameStats.timing.updateMs) ms")
+print("Render: \(frameStats.timing.renderTotalMs) ms")
+
+

OOC And Asset Triage Mode

+

High-volume instrumentation categories are disabled by default:

+
    +
  • OOCTiming
  • +
  • OOCStatus
  • +
  • AssetLoader
  • +
+

Enable them when diagnosing OOC/loader behavior:

+
// Keep structured profiler metrics on
+enableEngineMetrics = true
+setEngineStatsLogging(enabled: true, profile: .compact, intervalSeconds: 1.0)
+
+// Add focused trace logs
+Logger.enable(category: .oocStatus)   // OutOfCore lifecycle/status
+Logger.enable(category: .oocTiming)   // OOC timing detail
+Logger.enable(category: .assetLoader) // progressive loader parse/upload
+
+

Disable after capture:

+
Logger.disable(category: .oocTiming)
+Logger.disable(category: .oocStatus)
+Logger.disable(category: .assetLoader)
+
+

Instruments Workflow

+

When metrics are enabled, the engine emits signpost scopes:

+
    +
  • Frame
  • +
  • Update
  • +
  • RenderPrep
  • +
  • Encode
  • +
  • Submit
  • +
+

To inspect timeline data:

+
    +
  1. Open Instruments
  2. +
  3. Choose Points of Interest
  4. +
  5. Filter subsystem to com.untoldengine.profiling
  6. +
  7. Run the app with enableEngineMetrics = true (or UNTOLD_METRICS=1)
  8. +
+

Build Configuration Notes

+
    +
  • EngineProfiler is available in all configs, but disabled by default until enabled at runtime.
  • +
  • EngineStats collection is compiled in debug by default (ENGINE_STATS_ENABLED).
  • +
  • For release profiling with EngineStats, build with -DENGINE_STATS_ENABLED.
  • +
+

If ENGINE_STATS_ENABLED is not compiled in, getEngineStatsSnapshot() returns default values and setEngineStatsLogging(...) is effectively a no-op.

+

Category Toggle Notes

+
    +
  • Category filtering applies to Logger.log(...) debug/info trace paths.
  • +
  • Warnings and errors still emit regardless of category state.
  • +
  • Logger messages are lazily evaluated, so disabled categories avoid message-building cost.
  • +
+

Debug Helper (DEBUG Only)

+
#if DEBUG
+let metricsLogger = MetricsDebugLogger()
+metricsLogger.logIfNeeded() // throttle-prints approximately once per second
+#endif
+
+

Integrated Systems

+

Profiler hooks are already integrated into:

+
    +
  • UntoldEngine.swift (runFrame)
  • +
  • RenderingSystem.swift (UpdateRenderingSystem)
  • +
  • UntoldEngineXR.swift (executeXRSystemPass)
  • +
  • UntoldEngineAR.swift (draw)
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingRegistrationSystem/index.html b/site/API/UsingRegistrationSystem/index.html new file mode 100644 index 00000000..63e41ff7 --- /dev/null +++ b/site/API/UsingRegistrationSystem/index.html @@ -0,0 +1,2161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Registration System - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Using the Registration System in Untold Engine

+

The Registration System in the Untold Engine is an integral part of its Entity-Component-System (ECS) architecture. It provides core functionalities to manage entities and components, such as:

+
    +
  • Creating and destroying entities.
  • +
  • Registering components to entities.
  • +
  • Setting up helper functions for other systems by configuring necessary components.
  • +
+

How to Use the Registration System

+

Step 1: Create an Entity

+

Entities represent objects in the scene. Use the createEntity() function to create a new entity.

+
let entity = createEntity()
+
+
+

Step 2: Register Components

+

Components define the behavior or attributes of an entity. Use registerComponent to add a component to an entity.

+

registerComponent(entityId: entity, componentType: RenderComponent.self)
+
+Example:

+

When you load a mesh for rendering, the system automatically registers the required components:

+
setEntityMesh(entityId: entity, filename: "model", withExtension: "untold")
+
+

This function:

+
    +
  • Loads the mesh from the specified .untold file.
  • +
  • Associates the mesh with the entity.
  • +
  • Registers default components like RenderComponent and TransformComponent.
  • +
+
+

Step 3: Destroy an Entity

+

To remove an entity and its components from the scene, use destroyEntity.

+
destroyEntity(entityId: entity)
+
+

This ensures the entity is properly removed from all systems.

+
+

Step 4: Destroy All Entities Safely

+

Use destroyAllEntities(completion:) when you need to clear the world before loading new content.

+
destroyAllEntities {
+    // Safe point: pending destroys have been finalized.
+    // Load new content here (.untold, deserializeScene, etc).
+}
+
+

Important behavior:

+
    +
  • destroyAllEntities is a deferred operation. Entities are marked for destroy first.
  • +
  • Final cleanup runs during the engine frame finalization step (finalizePendingDestroys()).
  • +
  • The completion block runs only after that finalization step has finished.
  • +
+

This prevents race conditions where new entities are created while old entities are still pending destroy.

+

Example: clear world, then load a new .untold asset

+
destroyAllEntities {
+    let entity = createEntity()
+    setEntityMeshAsync(entityId: entity, filename: "office", withExtension: "untold")
+}
+
+

Example: playSceneAt pattern

+
public func playSceneAt(url: URL, completion: (() -> Void)? = nil) {
+    guard let scene = loadGameScene(from: url) else {
+        completion?()
+        return
+    }
+
+    destroyAllEntities {
+        deserializeScene(sceneData: scene) {
+            completion?()
+        }
+
+        // Early camera rebind during async mesh loading window.
+        CameraSystem.shared.activeCamera = findGameCamera()
+    }
+}
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingRenderingSystem/index.html b/site/API/UsingRenderingSystem/index.html new file mode 100644 index 00000000..7c04866e --- /dev/null +++ b/site/API/UsingRenderingSystem/index.html @@ -0,0 +1,2277 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Rendering System - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Enabling Rendering System in Untold Engine

+

The Rendering System in the Untold Engine is responsible for displaying your models on the screen. It supports advanced features such as Physically Based Rendering (PBR) for realistic visuals and multiple types of lights to illuminate your scenes.

+

How to Enable the Rendering System

+

Step 1: Create an Entity

+

Start by creating an entity that represents your 3D object.

+

let entity = createEntity()
+

+ +

To display a model, load its .untold runtime asset and link it to the entity using setEntityMesh.

+
setEntityMesh(entityId: entity, filename: "entity", withExtension: "untold")
+
+

Parameters:

+
    +
  • entityId: The ID of the entity created earlier.
  • +
  • filename: The name of the .untold file without the extension.
  • +
  • withExtension: The file extension, typically "untold" for runtime assets.
  • +
+
+

Note: If PBR textures (e.g., albedo, normal, roughness, metallic maps) are included, the rendering system will automatically use the appropriate PBR shader to render the model with realistic lighting and material properties.

+
+
+

Running the Rendering System

+

Once everything is set up:

+
    +
  1. Run the project.
  2. +
  3. Your model will appear in the game window, illuminated by the configured lights.
  4. +
  5. If the model is not visible or appears flat, revisit the lighting and texture setup to ensure everything is loaded correctly.
  6. +
+
+

Common Issues and Fixes

+

Issue: My Model Isn’t Visible!

+
    +
  • Cause: The scene lacks a light source.
  • +
  • Solution: Add a directional or point light as shown above. Lighting is required to render objects visibly.
  • +
+

Issue: Model Appears Flat or Dull

+
    +
  • Cause: PBR textures are missing or not linked properly.
  • +
  • Solution: Ensure the .untold asset references the correct PBR textures, and verify their paths during the loading process.
  • +
+

Debugging Tip:

+
    +
  • Log the addition of lights and entities to verify the scene setup.
  • +
  • Ensure the position of the point light is within the visible range of the camera and the objects it is meant to illuminate.
  • +
+
+

Tips and Best Practices

+
    +
  • Combine Light Types: Use directional lights for overall scene lighting and point lights for localized effects.
  • +
  • Use PBR Materials: Provide high-quality PBR textures for realistic rendering.
  • +
  • Position Lights Intelligently: Place point lights strategically to highlight key areas without excessive overlap.
  • +
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingScenegraph/index.html b/site/API/UsingScenegraph/index.html new file mode 100644 index 00000000..83ceb174 --- /dev/null +++ b/site/API/UsingScenegraph/index.html @@ -0,0 +1,2064 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Scene Graph - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Adding Parent-Child Relationships in Untold Engine

+

The Untold Engine includes a Scene Graph data structure, designed to manage hierarchical transformations efficiently. This enables parent-child relationships between entities, where a child's transformation (position, rotation, scale) is relative to its parent. For example, a car's wheels (children) move and rotate relative to the car body (parent).

+

Why Use Parent-Child Relationships?

+

Parent-child relationships are useful when you want multiple entities to move or transform together. When a parent entity changes its position, rotation, or scale, its child entities inherit those changes automatically. This is ideal for scenarios like:

+
    +
  • A car (parent) and its wheels (children)
  • +
  • A robot (parent) with movable arms and legs (children)
  • +
  • A group of objects that should remain in a fixed configuration relative to each other
  • +
+

Assigning Parent-Child Relationships

+

To assign a parent to an entity, use the setParent function. This function establishes a hierarchical relationship between the specified entities.

+
// Create child and parent entities
+let childEntity = createEntity()
+let parentEntity = createEntity()
+
+// Set parent-child relationship
+setParent(childId: childEntity, parentId: parentEntity)
+
+

What Happens Behind the Scenes?

+
    +
  1. Transformation Inheritance:
  2. +
  3. Once the relationship is established, any transformation applied to the parent entity (e.g., movement, rotation) will automatically affect the child entity.
  4. +
  5. +

    The child’s transformation is expressed relative to the parent.

    +
  6. +
  7. +

    Independent Local Transformations:

    +
  8. +
  9. While the child inherits the parent's transformations, it can also have its own independent local transformations, such as offset positions or rotations relative to the parent.
  10. +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingSpatialInput/index.html b/site/API/UsingSpatialInput/index.html new file mode 100644 index 00000000..52e96561 --- /dev/null +++ b/site/API/UsingSpatialInput/index.html @@ -0,0 +1,2943 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Spatial Input - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Spatial Input (vision Pro)

+

Spatial input in Untold Engine follows a simple pipeline:

+
    +
  1. visionOS emits raw spatial events.
  2. +
  3. UntoldEngineXR converts each event into an XRSpatialInputSnapshot.
  4. +
  5. Snapshots are queued in InputSystem.
  6. +
  7. XRSpatialGestureRecognizer processes snapshots each frame.
  8. +
  9. The engine publishes a single XRSpatialInputState your game reads in handleInput().
  10. +
+

That separation keeps the system flexible: the OS-facing code stays in UntoldEngineXR, while gesture classification stays in +the recognizer.

+

What You Get in Game Code

+

From XRSpatialInputState, you can read:

+
    +
  • spatialTapActive
  • +
  • spatialDragActive
  • +
  • spatialPinchActive
  • +
  • spatialPinchDragDelta
  • +
  • spatialZoomActive + spatialZoomDelta
  • +
  • spatialRotateActive + spatialRotateDeltaRadians
  • +
  • pickedEntityId
  • +
+

So your game logic can stay focused on behavior (select, move, rotate, scale), not event parsing.

+

Important Setup Step

+

You must enable XR event ingestion:

+

InputSystem.shared.registerXREvents()

+

If you skip this, the callback still receives OS events, but the engine ignores them.

+

Typical Frame Usage

+

In your handleInput():

+
    +
  • Poll InputSystem.shared.xrSpatialInputState.
  • +
  • React to edge-triggered gestures like tap.
  • +
  • Apply continuous updates for drag/zoom/rotate while active.
  • +
+

For object manipulation, use SpatialManipulationSystem for robust pinch-driven transforms, then layer custom behavior on top +when needed.

+

Quick Example

+

This example shows how to drag and rotate a mesh using the engine:

+
func handleInput() {
+    if gameMode == false { return }
+
+    let state = InputSystem.shared.xrSpatialInputState
+
+    if state.spatialTapActive, let entityId = state.pickedEntityId {
+        Logger.log(message: "Tapped entity: \(entityId)")
+    }
+
+    // Handles drag-based translate + twist rotation on picked entity
+    SpatialManipulationSystem.shared.processPinchTransformLifecycle(from: state)
+}
+
+

What This Does

+
    +
  • Tap → selects entity (via raycast picking)
  • +
  • Pinch + Drag → translates entity in world space
  • +
  • Pinch + Twist → rotates entity around a computed axis
  • +
+

processPinchTransformLifecycle handles:

+
    +
  • Begin
  • +
  • Update
  • +
  • End
  • +
  • Cancel
  • +
+

This lifecycle model prevents stuck manipulation sessions.

+
+

Manipulate Parent Instead Of Picked Child

+

If ray picking hits a child mesh and you want to manipulate the parent +actor:

+
var state = InputSystem.shared.xrSpatialInputState
+
+if let picked = state.pickedEntityId,
+   let parent = getEntityParent(entityId: picked) {
+    state.pickedEntityId = parent
+}
+
+SpatialManipulationSystem.shared.processPinchTransformLifecycle(from: state)
+
+

This is useful when:

+
    +
  • A character has multiple meshes
  • +
  • A building has sub-meshes
  • +
  • You want to move the root actor instead of individual geometry + pieces
  • +
+
+

Important Note

+

Do not early-return only because pickedEntityId == nil before calling +lifecycle processing.

+

End/cancel phases must still propagate to properly close manipulation +sessions.\ +Failing to do so can leave the engine in an inconsistent transform +state.

+
+

Picking Participation And Hit Representation

+

Use these APIs to control whether an entity can be selected by spatial tap/ray picking and what hit representation it uses.

+
setEntityPickParticipation(entityId: entityId, enabled: false) // visible, not pickable
+setEntityPickHitRepresentationMode(entityId: entityId, mode: .bounds) // pick using bounds
+setEntityPickHitRepresentationMode(entityId: entityId, mode: .mesh) // pick using mesh (default)
+
+

Available APIs:

+
    +
  • setEntityPickParticipation(entityId:enabled:)
  • +
  • getEntityPickParticipation(entityId:)
  • +
  • setEntityPickHitRepresentationMode(entityId:mode:)
  • +
  • getEntityPickHitRepresentationMode(entityId:)
  • +
+

Hit representation modes:

+
    +
  • .none\ + Never pickable.
  • +
  • .bounds\ + Pick using bounds intersection.
  • +
  • .mesh\ + Pick using mesh-capable path (default behavior).
  • +
+

Behavior rules:

+
    +
  • Default for existing entities: pick participation is enabled, hit mode is .mesh.
  • +
  • enabled == false means the entity is never returned by picking, regardless of mode.
  • +
  • mode == .none also means the entity is never returned by picking.
  • +
  • CPU and octree/GPU-preferred backends both respect these settings.
  • +
+
+

Raw Gesture Examples

+

It is strongly recommended to use the Spatial Helper functions instead +of raw gesture access.

+

Raw access is useful when:

+
    +
  • You want custom manipulation behavior
  • +
  • You are building a custom editor
  • +
  • You want non-standard gesture responses
  • +
+
+

Tap (Selection)

+

Vision Pro air-tap gesture.

+
let state = InputSystem.shared.xrSpatialInputState
+if state.spatialTapActive, let entityId = state.pickedEntityId {
+    // selectEntity(entityId)
+}
+
+

Use this to:

+
    +
  • Select objects
  • +
  • Trigger UI
  • +
  • Activate gameplay logic
  • +
+
+

Pinch Active

+

Single-hand pinch detected.

+
if InputSystem.shared.hasSpatialPinch() {
+    // pinch is active
+}
+
+

This does not imply dragging yet --- only that a pinch is currently +held.

+
+

Pinch Position

+

World-space position of pinch.

+
if let pinchPosition = InputSystem.shared.getPinchPosition() {
+    // use pinchPosition
+}
+
+

Useful for:

+
    +
  • Placing objects
  • +
  • Spawning actors
  • +
  • Visual debugging
  • +
+
+

Pinch Drag Delta

+

Drag delta while pinch is active.

+
let state = InputSystem.shared.xrSpatialInputState
+if state.spatialPinchActive {
+    let dragDelta = InputSystem.shared.getPinchDragDelta()
+    // app-defined translation/scaling response
+}
+
+

Common use cases:

+
    +
  • Translate object along plane
  • +
  • Move UI panels
  • +
  • Drag actors in world space
  • +
+
+

Anchored Pinch Drag Helper

+

For stable translation (no per-frame delta accumulation), use the +anchored lifecycle helper:

+
func handleInput() {
+    let state = InputSystem.shared.xrSpatialInputState
+
+    SpatialManipulationSystem.shared.processAnchoredPinchDragLifecycle(
+        from: state,
+        entityId: sceneRootEntity
+    )
+}
+
+

This helper:

+
    +
  • Captures initial hand + entity world positions
  • +
  • Applies absolute displacement from gesture start
  • +
  • Cleans up session state on end/cancel
  • +
+

Use this when moving large roots (buildings/scenes) where incremental +delta jitter can become visible.

+
+

Anchored Scene Drag Helper

+

For translating the entire scene root (rather than a single entity), use the anchored scene drag lifecycle:

+
func handleInput() {
+    let state = InputSystem.shared.xrSpatialInputState
+
+    SpatialManipulationSystem.shared.processAnchoredSceneDragLifecycle(from: state)
+}
+
+

This helper:

+
    +
  • Captures initial hand + scene root world positions on drag start
  • +
  • Applies absolute displacement from gesture start via translateSceneTo, keeping static batches intact
  • +
  • Cleans up session state on end/cancel
  • +
+

You can adjust movement speed with the sensitivity parameter (defaults to 1.0):

+
SpatialManipulationSystem.shared.processAnchoredSceneDragLifecycle(from: state, sensitivity: 0.5)
+
+

To manually end the drag (e.g. on a mode change), call:

+
SpatialManipulationSystem.shared.endAnchoredSceneDrag()
+
+

Use this when panning an entire scene — for example, sliding a map, architectural model, or level layout in world space.

+
+

Anchored Scene Rotate Helper

+

For rotating the entire scene root around world up (+Y) while preserving static batching, use the anchored scene rotate lifecycle. This requires a two-hand pinch + twist gesture (spatialRotateActive with both hands pinching):

+
func handleInput() {
+    let state = InputSystem.shared.xrSpatialInputState
+
+    SpatialManipulationSystem.shared.processAnchoredSceneRotateLifecycle(from: state)
+}
+
+

This helper:

+
    +
  • Activates only when both hands are pinching and a two-hand rotate gesture is recognized
  • +
  • Captures the initial two-hand vector direction + scene yaw on rotate start
  • +
  • Applies absolute yaw from gesture start via rotateSceneToYaw, keeping static batches intact
  • +
  • Ends automatically when either hand releases or the rotate gesture ends
  • +
+

You can adjust rotation speed with the sensitivity parameter (defaults to 1.0):

+
SpatialManipulationSystem.shared.processAnchoredSceneRotateLifecycle(from: state, sensitivity: 0.5)
+
+

To manually end rotation (e.g. on a mode change), call:

+
SpatialManipulationSystem.shared.endAnchoredSceneRotate()
+
+

Use this when aligning or calibrating an already-loaded large scene in place without rebatching.

+
+

Unified Scene Manipulation Helper

+

To avoid drag/rotate gesture fighting, use the unified scene-root manipulation lifecycle:

+
func handleInput() {
+    let state = InputSystem.shared.xrSpatialInputState
+
+    SpatialManipulationSystem.shared.processAnchoredSceneManipulationLifecycle(
+        from: state,
+        dragSensitivity: 1.0,
+        rotateSensitivity: 0.5
+    )
+}
+
+

Arbitration rules:

+
    +
  • When a pinch is first detected, classification is deferred for a few frames (manipulationClassificationFrames, default 3) so the second hand has time to arrive
  • +
  • Two-hand pinch + twist (spatialRotateActive + both hands pinching) routes to scene rotate
  • +
  • Otherwise, after the deferral window expires, pinch drag routes to scene drag
  • +
  • The non-winning session is ended automatically
  • +
  • Once a mode is chosen, it stays latched (drag or rotate) until the gesture ends/release happens
  • +
+

You can tune the deferral window (set to 0 to commit immediately):

+
SpatialManipulationSystem.shared.manipulationClassificationFrames = 4  // ~44ms at 90 Hz
+
+

To manually end the unified lifecycle (e.g. on a mode change), call:

+
SpatialManipulationSystem.shared.endAnchoredSceneManipulation()
+
+

Use this as the default scene-root helper when your app supports both panning and rotation.

+
+

Combining Scene Drag, Rotate and Zoom

+

All three scene-level gestures can live in the same input loop — they gate on different input conditions so they don't conflict:

+
func handleInput() {
+    let state = InputSystem.shared.xrSpatialInputState
+
+    // Single-hand pinch + drag → pan the scene
+    SpatialManipulationSystem.shared.processAnchoredSceneDragLifecycle(from: state)
+
+    // Two-hand pinch + twist → rotate the scene (yaw)
+    SpatialManipulationSystem.shared.processAnchoredSceneRotateLifecycle(from: state)
+
+    // Two-hand pinch + spread/pinch → zoom an entity
+    SpatialManipulationSystem.shared.applyTwoHandZoomIfNeeded(from: state)
+}
+
+

For context-based entity vs. scene rotation — route two-hand twist to entity rotate when something is picked, and to scene rotate otherwise:

+
func handleInput() {
+    let state = InputSystem.shared.xrSpatialInputState
+
+    // Scene-level drag (always active)
+    SpatialManipulationSystem.shared.processAnchoredSceneDragLifecycle(from: state)
+
+    if state.pickedEntityId != nil {
+        // Entity is picked → two-hand twist rotates the entity
+        SpatialManipulationSystem.shared.applyTwoHandRotateIfNeeded(from: state)
+    } else {
+        // Nothing picked → two-hand twist rotates the scene
+        SpatialManipulationSystem.shared.processAnchoredSceneRotateLifecycle(from: state)
+    }
+
+    SpatialManipulationSystem.shared.applyTwoHandZoomIfNeeded(from: state)
+}
+
+
+

Two-Hand Zoom

+

Apply the built-in zoom response:

+
let state = InputSystem.shared.xrSpatialInputState
+
+SpatialManipulationSystem.shared.applyTwoHandZoomIfNeeded(
+    from: state,
+    sensitivity: 1.0
+)
+
+

By default, the helper scales the parent of the picked entity when available. +If you want to choose the exact target, pass entityId:

+
let state = InputSystem.shared.xrSpatialInputState
+
+if let picked = state.pickedEntityId {
+    // Scale exactly what was hit
+    SpatialManipulationSystem.shared.applyTwoHandZoomIfNeeded(
+        from: state,
+        entityId: picked,
+        sensitivity: 1.0
+    )
+
+    // Or scale its parent explicitly
+    if let parent = getEntityParent(entityId: picked) {
+        SpatialManipulationSystem.shared.applyTwoHandZoomIfNeeded(
+            from: state,
+            entityId: parent,
+            sensitivity: 1.0
+        )
+    }
+}
+
+
+

Two-Hand Rotate

+

Use setXRTwoHandRotateAxisMode to control how the rotation axis is derived:

+
InputSystem.shared.setXRTwoHandRotateAxisMode(.dynamicSnapped)
+
+

Available modes:

+
    +
  • .cameraForward: rotates around camera-forward axis (screen-style twist)
  • +
  • .dynamic: derives axis from actual two-hand motion
  • +
  • .dynamicSnapped: dynamic axis snapped to dominant world axis (x, y, or z)
  • +
+

Apply the built-in rotate response:

+
let state = InputSystem.shared.xrSpatialInputState
+
+SpatialManipulationSystem.shared.applyTwoHandRotateIfNeeded(
+    from: state,
+    sensitivity: 1.5
+)
+
+

By default, the helper rotates the parent of the picked entity when available. +If you want to choose the exact target, pass entityId:

+
let state = InputSystem.shared.xrSpatialInputState
+
+if let picked = state.pickedEntityId {
+    // Rotate exactly what was hit
+    SpatialManipulationSystem.shared.applyTwoHandRotateIfNeeded(
+        from: state,
+        entityId: picked,
+        sensitivity: 1.5
+    )
+
+    // Or rotate its parent explicitly
+    if let parent = getEntityParent(entityId: picked) {
+        SpatialManipulationSystem.shared.applyTwoHandRotateIfNeeded(
+            from: state,
+            entityId: parent,
+            sensitivity: 1.5
+        )
+    }
+}
+
+

Get distance to hit-entity

+

To get the distance to an entity use the following:

+
// Get distance to hit-entity
+let state = InputSystem.shared.xrSpatialInputState
+if state.spatialTapActive, let entityId = state.pickedEntityId {
+    // get distance
+    let distance = state.pickedEntityDistance
+    print("Object distance: \(distance) meters")
+}
+
+
+

Get Ground/Plane Hit Position

+

To retrieve the exact world-space position where the user taps on the ground, use pickRealSurfacePosition. This is useful for calibration workflows where you need to anchor a point on the ground and scale a model relative to it.

+
let state = InputSystem.shared.xrSpatialInputState
+
+if state.spatialTapActive{
+    if let hit = pickRealSurfacePosition(
+          rayOrigin: state.rayOriginWorld,
+          rayDirection: state.rayDirectionWorld,
+          filter: .horizontalAny
+      ) {
+          Logger.log(message: "Surface type: \(hit.surfaceKind)", vector: hit.worldPosition)
+      }
+}
+
+
+

Spatial Helper Functions

+

Use these helpers from SpatialManipulationSystem.shared:

+
    +
  • +

    processPinchTransformLifecycle(from:)\ + Recommended default. Handles translation + twist rotation lifecycle + safely.

    +
  • +
  • +

    applyPinchDragIfNeeded(from:entityId:sensitivity:)\ + Lower-level translation helper if you want full control.

    +
  • +
  • +

    processAnchoredSceneDragLifecycle(from:sensitivity:)\ + Anchored drag for the entire scene root. Applies absolute + displacement via translateSceneTo.

    +
  • +
  • +

    endAnchoredSceneDrag()\ + Manually ends an in-progress anchored scene drag session.

    +
  • +
  • +

    processAnchoredSceneRotateLifecycle(from:sensitivity:)\ + Anchored rotate for the entire scene root using two-hand pinch + twist. + Applies absolute yaw via rotateSceneToYaw.

    +
  • +
  • +

    endAnchoredSceneRotate()\ + Manually ends an in-progress anchored scene rotate session.

    +
  • +
  • +

    processAnchoredSceneManipulationLifecycle(from:dragSensitivity:rotateSensitivity:)\ + Unified scene-root helper with drag/rotate arbitration to prevent + gesture-fighting. Uses a deferral window (manipulationClassificationFrames) before + committing to drag so the second hand has time to arrive for rotate.

    +
  • +
  • +

    endAnchoredSceneManipulation()\ + Ends any in-progress unified scene manipulation (drag, rotate, or pending classification).

    +
  • +
  • +

    applyTwoHandZoomIfNeeded(from:sensitivity:)\ + Provides zoom delta signal. You must define what zoom means in your + app.

    +
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingStaticBatchingSystem/index.html b/site/API/UsingStaticBatchingSystem/index.html new file mode 100644 index 00000000..b64f2b5b --- /dev/null +++ b/site/API/UsingStaticBatchingSystem/index.html @@ -0,0 +1,2365 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Static Batching - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Static Batching System

+

UntoldEngine supports two batching modes in practice:

+ + + + + + + + + + + + + + + + + +
ModeUse for
Manual batch generationAlways-resident static content
Runtime cell-based batchingTiled streaming scenes
+

Manual Batching for Always-Resident Content

+

Mark loaded entities as static, enable batching, then build the initial artifacts.

+
let cube1 = createEntity()
+setEntityMesh(entityId: cube1, filename: "cube", withExtension: "untold")
+translateTo(entityId: cube1, position: simd_float3(0, 0, 0))
+setEntityStaticBatchComponent(entityId: cube1)
+
+let cube2 = createEntity()
+setEntityMesh(entityId: cube2, filename: "cube", withExtension: "untold")
+translateTo(entityId: cube2, position: simd_float3(2, 0, 0))
+setEntityStaticBatchComponent(entityId: cube2)
+
+enableBatching(true)
+generateBatches()
+
+

For async loading, mark entities static in the completion block:

+
let building = createEntity()
+
+setEntityMeshAsync(entityId: building, filename: "office_building", withExtension: "untold") { success in
+    guard success else { return }
+    setEntityStaticBatchComponent(entityId: building)
+    enableBatching(true)
+    generateBatches()
+}
+
+

Runtime Batching in Tiled Scenes

+

For tiled scenes, the flow is different:

+
let sceneRoot = createEntity()
+setEntityName(entityId: sceneRoot, name: "city")
+
+setEntityStreamScene(entityId: sceneRoot, manifest: "city", withExtension: "json") { success in
+    setSceneReady(success)
+}
+
+

In this mode:

+
    +
  • registerTiledScene(...) enables batching automatically
  • +
  • full-load tiles notify batching through notifyTileEntitiesResident(_:)
  • +
  • OCC sub-mesh uploads join batching incrementally through normal residency events
  • +
  • per-tile LOD and HLOD representations can also participate when enabled
  • +
+

You do not call generateBatches() every time a tile loads. The batching system rebuilds dirty cells incrementally based on residency changes.

+

Streamed vs Non-Streamed Scenes

+

For tiled/streamed scenes, the engine manages static batching automatically. When a tile finishes loading, the engine assigns a StaticBatchComponent to all of its entities and schedules an incremental batch rebuild for only the spatial cells affected by that tile. This happens internally on a background queue via the engine's tick() loop.

+

Do not call generateBatches() for streamed scenes. That function performs a full global rebuild — it queries every entity in the scene simultaneously, merges entities from different tiles into shared batch groups, and allocates all GPU buffers synchronously on the render thread. This overrides the engine's incremental system and causes a noticeable stall.

+

For streamed scenes, only call enableBatching(true) after the scene loads. The engine handles the rest:

+
setEntityStreamScene(entityId: sceneRoot, manifest: "city", withExtension: "json") { success in
+    enableBatching(true)
+    setSceneReady(success)
+}
+
+

For non-streamed scenes (single .untold), call setEntityStaticBatchComponent, generateBatches(), and enableBatching(true) as normal. The same applies to any operation that mutates material state (color, opacity) — wrap it with enableBatching(false) before and generateBatches() + enableBatching(true) after, but only for non-streamed scenes:

+
// Non-streamed only — do not use this pattern in tiled/streamed scenes
+enableBatching(false)
+setEntityColor(entityId: prop, color: simd_float4(1, 0, 0, 1))
+generateBatches()
+enableBatching(true)
+
+

Core APIs

+

setEntityStaticBatchComponent(entityId:)

+

Marks an entity hierarchy as eligible for batching.

+
setEntityStaticBatchComponent(entityId: entity)
+
+

removeEntityStaticBatchComponent(entityId:)

+

Removes static batching tags from the entity hierarchy.

+
removeEntityStaticBatchComponent(entityId: entity)
+
+

enableBatching(_:)

+

Globally enables or disables runtime batching.

+
enableBatching(true)
+
+

generateBatches()

+

Builds batch artifacts for the currently marked static entities. This is mainly for always-resident/manual workflows.

+
generateBatches()
+
+

clearSceneBatches()

+

Clears all generated batch artifacts.

+
clearSceneBatches()
+
+

Good Candidates

+
    +
  • environment geometry
  • +
  • buildings and structures
  • +
  • terrain chunks
  • +
  • furniture and static props
  • +
+

Poor Candidates

+
    +
  • characters and NPCs
  • +
  • vehicles
  • +
  • projectiles
  • +
  • animated or skinned meshes
  • +
  • objects that move frequently
  • +
+

Notes for the New Architecture

+
    +
  • The batching system is now cell-based and visibility-gated.
  • +
  • Tile streaming and batching are tightly integrated; residency events are no longer the old per-entity event storm for full-load tiles.
  • +
  • TileLODTagComponent lets batching treat per-tile LODs and HLODs as distinct LOD groups even though they are not entity-level LODComponent assets.
  • +
+

For architectural details, see Batching System.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingSteeringSystem/index.html b/site/API/UsingSteeringSystem/index.html new file mode 100644 index 00000000..ad06826a --- /dev/null +++ b/site/API/UsingSteeringSystem/index.html @@ -0,0 +1,2238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Steering System - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Using the Steering System in Untold Engine

+

The Steering System in the Untold Engine enables entities to move dynamically and intelligently within the scene. It provides both low-level steering behaviors (e.g., seek, flee, arrive) for granular control and high-level behaviors (e.g., steerTo, steerAway, followPath) that integrate seamlessly with the Physics System.

+

Why Use the Steering System?

+

The Steering System is essential for creating dynamic and realistic movement for entities, such as:

+
    +
  • A character chasing a target.
  • +
  • An enemy avoiding obstacles.
  • +
  • A vehicle following a predefined path.
  • +
+

The high-level behaviors are recommended because they are designed to work closely with the Physics System, simplifying implementation while maintaining smooth motion.

+
+

How to Use the Steering System

+

Examples:

+
    +
  1. Steer Toward a Target Position:
  2. +
+

steerSeek(entityId: entity, targetPosition: targetPosition, maxSpeed: 5.0, deltaTime: 0.016)
+
+2. Steer Away from a Threat:

+
steerFlee(entityId: entity, threatPosition: threatPosition, maxSpeed: 5.0, deltaTime: 0.016)
+
+
    +
  1. Follow a Path: Guide an entity along a series of waypoints.
  2. +
+

steerFollowPath(entityId: entity, path: waypoints, maxSpeed: 5.0, deltaTime: 0.016)
+
+4. Pursue a Moving Target:

+
steerPursuit(entityId: chaserEntity, targetEntity: targetEntity, maxSpeed: 5.0, deltaTime: 0.016)
+
+
    +
  1. Avoid Obstacles:
  2. +
+
steerAvoidObstacles(entityId: entity, obstacles: obstacleEntities, avoidanceRadius: 2.0, maxSpeed: 5.0, deltaTime: 0.016)
+
+
    +
  1. Steer Toward a Target Position (with Arrive):
  2. +
+
steerArrive(entityId: entity, targetPosition: targetPosition, maxSpeed: 5.0, deltaTime: 0.016)
+
+
    +
  1. Steer using WASD keys
  2. +
+
steerWithWASD(entityId: entity, maxSpeed: 5.0, deltaTime: 0.016)
+
+
+

What Happens Behind the Scenes?

+
    +
  1. Low-Level Behaviors:
  2. +
  3. Calculate desired velocity based on the target or threat position.
  4. +
  5. Generate steering forces by comparing desired velocity with current velocity.
  6. +
  7. High-Level Behaviors:
  8. +
  9. Use low-level behaviors to calculate steering adjustments.
  10. +
  11. Apply these forces to the entity’s physics system for smooth, realistic motion.
  12. +
  13. Align the entity’s orientation to face its movement direction.
  14. +
  15. Physics Integration:
  16. +
  17. Forces are applied through the Physics System, ensuring that movement respects mass, velocity, and acceleration.
  18. +
+
+

Tips and Best Practices

+
    +
  • Prefer High-Level Behaviors: They simplify complex movement patterns and automatically handle integration with the Physics System.
  • +
  • Use Low-Level Behaviors for Custom Logic: When precise control is required, combine low-level behaviors for unique movement styles.
  • +
  • Smooth Orientation: Use alignOrientation or integrate orientation alignment directly into high-level functions.
  • +
  • Tune Parameters: Adjust maxSpeed, turnSpeed, and slowingRadius for different entity types (e.g., fast-moving cars vs. slow-moving enemies).
  • +
+
+

Common Issues and Fixes

+

Issue: Entity Doesn’t Move

+
    +
  • Cause: The Physics Component is missing or paused.
  • +
  • Solution: Ensure the entity has a PhysicsComponents and it’s not paused.
  • +
+

Issue: Jittery Movement

+
    +
  • Cause: Conflicting forces or large delta times.
  • +
  • Solution: Tune maxSpeed and ensure deltaTime is passed correctly.
  • +
+

Issue: Entity Ignores Obstacles

+
    +
  • Cause: Avoidance radius is too small or obstacles are not registered.
  • +
  • Solution: Increase the avoidanceRadius and verify obstacle entities.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingTheExporter/index.html b/site/API/UsingTheExporter/index.html new file mode 100644 index 00000000..4c2bb989 --- /dev/null +++ b/site/API/UsingTheExporter/index.html @@ -0,0 +1,2228 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Exporter - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Using The Exporter

+

UntoldEngine ships two user-facing exporter commands in the scripts/ folder at the repo root:

+
    +
  • export-untold
  • +
  • export-untold-tiles
  • +
+

These wrappers launch Blender in background mode and run the Python exporters for you. Users should run the shell wrappers, not the raw Blender commands.

+

Prerequisites

+

Blender must be installed.

+

The wrappers resolve Blender in this order:

+
    +
  1. --blender /path/to/Blender
  2. +
  3. BLENDER_BIN=/path/to/Blender
  4. +
  5. /Applications/Blender.app/Contents/MacOS/Blender
  6. +
  7. blender on PATH
  8. +
+

If Blender cannot be found, the wrapper prints an install message and exits.

+

Export A Single Asset

+

Use export-untold (found in scripts/) to convert one USD or USDZ asset into one .untold runtime file.

+

Basic usage:

+
./scripts/export-untold \
+  --input /path/model.usdz \
+  --output /path/model.untold
+
+

Common options:

+
    +
  • --input <path>: required source .usd, .usda, .usdc, or .usdz
  • +
  • --output <path>: required destination .untold
  • +
  • --file-type <tile|lod|hlod|shared>: optional, defaults to tile
  • +
  • --mesh-name <name>: optional, export only one mesh from a multi-mesh asset
  • +
  • --ConvertOrientation: optional, convert the export into engine space
  • +
  • --source-orientation <blender-native|engine-oriented>: optional, defaults to blender-native
  • +
  • --validate: optional, also writes <name>.validation.json
  • +
  • --blender <path>: optional wrapper-level Blender override
  • +
+

Example:

+
./scripts/export-untold \
+  --input GameData/Models/robot/robot.usdz \
+  --output GameData/Models/robot/robot.untold \
+  --ConvertOrientation \
+  --source-orientation blender-native \
+  --validate
+
+

Expected output:

+
    +
  • robot.untold
  • +
  • Textures/... beside the .untold file if the asset uses textures
  • +
  • robot.validation.json only when --validate is passed
  • +
+

Export A Scene Into Tiles

+

Use export-untold-tiles (found in scripts/) to partition a USD or USDZ scene into tile payloads and generate a manifest JSON file.

+

Basic usage:

+
./scripts/export-untold-tiles \
+  --input /path/scene.usdz \
+  --output-dir /path/tile_exports \
+  --tile-size-x 25 \
+  --tile-size-y 10000 \
+  --tile-size-z 25
+
+

Common options:

+
    +
  • --input <path>: required source .usd, .usda, .usdc, or .usdz
  • +
  • --output-dir <path>: required destination directory for tile payloads
  • +
  • --tile-size-x <number>: optional tile width in world units
  • +
  • --tile-size-y <number>: optional tile height in world units, defaults to 10000
  • +
  • --tile-size-z <number>: optional tile depth in world units
  • +
  • --auto-tile-size: optional automatic tile sizing
  • +
  • --generate-hlod: optional HLOD generation
  • +
  • --generate-lod: optional per-tile LOD generation
  • +
  • --dry-run: optional planning pass without writing payload files
  • +
  • --write-manifest-in-dry-run: optional manifest write during dry run
  • +
  • --visible-only: optional export only visible meshes
  • +
  • --all-meshes: optional include hidden meshes
  • +
  • --debug-aabb-only: optional emit debug AABB payloads instead of geometry
  • +
  • --quadtree: optional partition tiles using a quad-tree instead of a uniform grid
  • +
  • --floor-count <number>: optional number of vertical floors to split each tile into
  • +
  • --blender <path>: optional wrapper-level Blender override
  • +
+

Example:

+
./scripts/export-untold-tiles \
+  --input GameData/Models/dungeon/dungeon.usdz \
+  --output-dir GameData/Models/dungeon/tile_exports \
+  --tile-size-x 25 \
+  --tile-size-y 10000 \
+  --tile-size-z 25 \
+  --generate-hlod \
+  --generate-lod
+
+

Dry-run example:

+
./scripts/export-untold-tiles \
+  --input GameData/Models/dungeon/dungeon.usdz \
+  --output-dir GameData/Models/dungeon/tile_exports \
+  --tile-size-x 25 \
+  --tile-size-y 10000 \
+  --tile-size-z 25 \
+  --dry-run \
+  --write-manifest-in-dry-run
+
+

Expected output layout:

+
    +
  • dungeon.json beside the tile payload directory
  • +
  • tile_exports/tile_*.untold
  • +
  • optional HLOD and LOD .untold files in tile_exports/
  • +
  • tile_exports/Textures/... for staged textures
  • +
+

The manifest stores relative runtime paths so it remains portable across machines, repos, and app bundles.

+

Optimization Workflows

+

After exporting assets, use Optimizations for optional +workflows such as ASTC texture compression and LZ4 geometry compression.

+

Loading The Result In The Engine

+

Single asset:

+
setEntityMeshAsync(
+    entityId: entityId,
+    filename: "robot",
+    withExtension: "untold"
+)
+
+

Tiled scene:

+
let sceneRoot = createEntity()
+setEntityName(entityId: sceneRoot, name: "dungeon")
+setEntityStreamScene(entityId: sceneRoot, manifest: "dungeon", withExtension: "json")
+
+

The manifest should live next to the tile payload directory. Tile, HLOD, LOD, and shared-bucket payloads are resolved relative to the manifest file.

+

Notes

+
    +
  • .untold tile payloads participate in the current tiled streaming architecture, including tile-level load/unload, remote download + cache, per-tile LOD/HLOD, and large-tile OCC sub-mesh streaming when the runtime classifies a tile into the OOC path.
  • +
  • The Python files in scripts/ are implementation details. The recommended user entry points are the shell wrappers in the same folder.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingTheLogger/index.html b/site/API/UsingTheLogger/index.html new file mode 100644 index 00000000..947d6017 --- /dev/null +++ b/site/API/UsingTheLogger/index.html @@ -0,0 +1,2385 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Logger - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Logger

+

UntoldEngine includes a thread-safe logger with log-level filtering, per-category toggles, and a sink API for routing log events to custom destinations (e.g. an in-editor console).

+

Log Levels

+

Log level controls the minimum severity that is emitted. Set it once at startup:

+
Logger.logLevel = .debug   // emit everything
+Logger.logLevel = .info    // emit info, warnings, and errors
+Logger.logLevel = .warning // emit warnings and errors only
+Logger.logLevel = .error   // emit errors only
+Logger.logLevel = .none    // suppress all output
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LevelValueWhat emits
.none0nothing
.error1errors
.warning2warnings + errors
.info3info + warnings + errors
.debug4everything
.test5everything (used in unit tests)
+

The default level is .debug.

+

Logging Messages

+

Info / general trace

+
Logger.log(message: "Scene loaded successfully", category: LogCategory.general.rawValue)
+
+

Requires logLevel >= .info. Suppressed if the category is disabled.

+

Warnings

+
Logger.logWarning(message: "Mesh has no UV channel", category: LogCategory.general.rawValue)
+
+

Requires logLevel >= .warning. Always emits regardless of category state.

+

Errors

+
Logger.logError(message: "Failed to load texture: \(name)", category: LogCategory.general.rawValue)
+
+

Requires logLevel >= .error. Always emits regardless of category state.

+
+

Note: Messages are lazily evaluated (@autoclosure), so string interpolation cost is skipped when the log would be suppressed.

+
+

Log Categories

+

Categories let you silence or focus specific subsystems without changing the global log level.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryRaw valueDefault state
.general"General"enabled
.ecs"ECS"enabled
.engineStats"EngineStats"enabled
.integration"Integration"enabled
.xrCamera"XRCamera"disabled
.oocTiming"OOCTiming"disabled
.oocStatus"OOCStatus"disabled
.assetLoader"AssetLoader"disabled
+

High-volume categories (xrCamera, oocTiming, oocStatus, assetLoader) are off by default to avoid log spam during normal operation.

+

Enabling and Disabling Categories

+
// Enable a category
+Logger.enable(category: .oocStatus)
+
+// Disable a category
+Logger.disable(category: .xrCamera)
+
+// Toggle with a Bool
+Logger.set(category: .assetLoader, enabled: true)
+
+// Check current state
+if Logger.isEnabled(category: .ecs) { ... }
+
+// Reset all overrides back to defaults
+Logger.resetCategoryToggles()
+
+

Typical debug session

+
// Turn on verbose streaming traces for a debug session
+Logger.enable(category: .oocStatus)
+Logger.enable(category: .oocTiming)
+Logger.enable(category: .assetLoader)
+
+// ... reproduce the issue ...
+
+// Clean up after capture
+Logger.disable(category: .oocStatus)
+Logger.disable(category: .oocTiming)
+Logger.disable(category: .assetLoader)
+
+

Adding a Custom Sink

+

Implement LoggerSink to route events to a custom destination such as an editor console or file:

+
final class ConsoleSink: LoggerSink {
+    func didLog(_ event: LogEvent) {
+        print("[\(event.category)] \(event.message)")
+    }
+}
+
+let sink = ConsoleSink()
+Logger.addSink(sink)
+
+

LogEvent exposes level, message, category, file, function, line, and timestamp.

+

Sinks are held weakly — the logger will not extend their lifetime.

+
+

Sink delivery and backlog replay are available on macOS (AppKit) builds only.

+
+

Category Toggle Notes

+
    +
  • Logger.log(...) respects both logLevel and category state.
  • +
  • Logger.logWarning(...) and Logger.logError(...) respect logLevel only — they are never suppressed by category.
  • +
  • Category overrides layer on top of the built-in defaults. Call resetCategoryToggles() to restore defaults without restarting.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingTransformSystem/index.html b/site/API/UsingTransformSystem/index.html new file mode 100644 index 00000000..ebda387e --- /dev/null +++ b/site/API/UsingTransformSystem/index.html @@ -0,0 +1,2573 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Transform System - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Using the Transform System in Untold Engine

+

The Transform System is a core part of the Untold Engine, responsible for managing the position, rotation, and scale of entities. It provides both local transformations (relative to a parent entity) and world transformations (absolute in the scene).

+

How to Use the Transform System

+

Step 1: Retrieve Transform Data

+

You can retrieve an entity’s position, orientation, or axis vectors using the provided functions.

+

Get Local Position

+

Retrieves the entity’s position relative to its parent.

+
let localPosition = getLocalPosition(entityId: entity)
+
+

Get World Position

+

Retrieves the entity’s absolute position in the scene.

+
let worldPosition = getPosition(entityId: entity)
+
+

Get Local Orientation

+

Retrieves the entity’s orientation matrix relative to its parent.

+
let localOrientation = getLocalOrientation(entityId: entity)
+
+

Get World Orientation

+

Retrieves the entity’s absolute orientation matrix.

+
let worldOrientation = getOrientation(entityId: entity)
+
+

Get Axis Vectors

+

Retrieve the entity’s forward, right, or up axis:

+
let forward = getForwardAxisVector(entityId: entity)
+let right = getRightAxisVector(entityId: entity)
+let up = getUpAxisVector(entityId: entity)
+
+
+

Step 2: Update Transform Data

+

Modify an entity’s transform by translating or rotating it.

+

Translate the Entity

+

Move the entity to a new position:

+
translateTo(entityId: entity, position: simd_float3(5.0, 0.0, 3.0))
+
+

Move the entity by an offset relative to its current position:

+
translateBy(entityId: entity, position: simd_float3(1.0, 0.0, 0.0))
+
+

Rotate the Entity

+

Rotate the entity to a specific angle around an axis:

+
rotateTo(entityId: entity, angle: 45.0, axis: simd_float3(0.0, 1.0, 0.0))
+
+

Apply an incremental rotation to the entity:

+
rotateBy(entityId: entity, angle: 15.0, axis: simd_float3(0.0, 1.0, 0.0))
+
+

Directly set the entity’s rotation matrix:

+
rotateTo(entityId: entity, rotation: simd_float4x4( /* matrix values */ ))
+
+
+

What Happens Behind the Scenes?

+
    +
  1. Local and World Transform Components:
  2. +
  3. Each entity has a LocalTransformComponent for transformations relative to its parent.
  4. +
  5. The WorldTransformComponent calculates the absolute transform by combining the local transform with the parent’s world transform.
  6. +
  7. Transform Matrices:
  8. +
  9. Transformations are stored in 4x4 matrices that include position, rotation, and scale.
  10. +
  11. These matrices are updated whenever you translate or rotate an entity.
  12. +
  13. Scene Graph Integration:
  14. +
  15. Changes to a parent entity automatically propagate to its children through the scene graph.
  16. +
+
+

Step 3: Translate the Entire Scene

+

These functions move the scene root without modifying individual entity transforms. Because per-entity transforms stay untouched, static batches remain intact and no rebatching is needed.

+

Move Scene to an Absolute Position

+
translateSceneTo(position: simd_float3(10.0, 0.0, 5.0))
+
+

Move Scene by a Relative Offset

+
translateSceneBy(delta: simd_float3(1.0, 0.0, 0.0))
+
+

Use these when you need to pan or reposition the entire world — for example, sliding a map or architectural model during a spatial drag gesture.

+
+

Step 4: Rotate the Entire Scene (Yaw)

+

These functions rotate the scene root around world up (+Y) without modifying individual entity transforms. Static batches remain intact and no rebatching is required.

+

Rotate Scene to an Absolute Yaw

+
rotateSceneToYaw(.pi / 2.0)
+
+

Rotate Scene by a Relative Yaw Delta

+
rotateSceneByYaw(.pi / 18.0) // +10 degrees
+
+

Use these when you need to align or calibrate a large loaded scene in-place (for example, Vision Pro room alignment) while keeping batching and culling efficient.

+
+

Step 5: Scene-Root Space Conversion and Reset Helpers

+

Use these helpers when converting between scene-local/entity space and visual world space:

+
let visual = sceneLocalToVisualWorld(simd_float3(1, 0, 0))
+let local = visualWorldToSceneLocal(visual)
+
+

To get an entity's visual world position (with scene-root transform applied):

+
let visualPosition = getVisualPosition(entityId: myEntity)
+
+

To return scene root to identity quickly:

+
resetSceneRootTransform()
+
+

This resets position/rotation/scale and refreshes root matrices immediately.

+
+

Tips and Best Practices

+
    +
  • Use Local Transformations for Hierarchies:
      +
    • For example, a car’s wheels (children) should use local transforms relative to the car body (parent).
    • +
    +
  • +
  • Combine Translations and Rotations:
      +
    • Use translateTo to set an entity's absolute position or rotation.
    • +
    • Use translateBy for incremental adjustments.
    • +
    +
  • +
  • Use Scene-Level Translation for Batch-Safe Movement:
      +
    • Use translateSceneTo / translateSceneBy instead of moving every entity individually.
    • +
    • This avoids breaking static batches and is ideal for spatial drag gestures on the whole scene.
    • +
    +
  • +
  • Use Scene-Level Yaw Rotation for Batch-Safe Alignment:
      +
    • Use rotateSceneToYaw / rotateSceneByYaw to rotate large scenes around world up.
    • +
    • This is ideal for scene alignment and calibration flows where static batching must stay active.
    • +
    +
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/API/UsingUntoldEngineCLI/index.html b/site/API/UsingUntoldEngineCLI/index.html new file mode 100644 index 00000000..bc84d5d8 --- /dev/null +++ b/site/API/UsingUntoldEngineCLI/index.html @@ -0,0 +1,2385 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Untold Engine CLI - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Using the UntoldEngine CLI

+

The untoldengine-create CLI tool scaffolds ready-to-run Xcode projects with UntoldEngine pre-configured. Instead of setting up package dependencies and boilerplate by hand, you run one command and get a fully wired project for your target platform.

+

The install script (scripts/install-untoldengine-create.sh) builds the CLI from source and places it in /usr/local/bin so it is available globally in your shell.

+
+

Requirements

+
    +
  • macOS 14.0 or later
  • +
  • Xcode 15.0 or later
  • +
  • Swift 6.0 or later
  • +
+
+

Installation

+

Clone the repository and run the install script from the repo root:

+
git clone https://github.com/untoldengine/UntoldEngine.git
+cd UntoldEngine
+./scripts/install-untoldengine-create.sh
+
+

The script will:

+
    +
  1. Build untoldengine-create in release mode using Swift Package Manager.
  2. +
  3. Copy the binary to /usr/local/bin (prompts for admin privileges if needed).
  4. +
  5. Mark it executable.
  6. +
  7. Verify that the tool is reachable on your PATH.
  8. +
+

If the final verification step warns that untoldengine-create is not found in PATH, add /usr/local/bin to your shell profile:

+
# Add to ~/.zshrc or ~/.bashrc
+export PATH="/usr/local/bin:$PATH"
+
+

Then reload your shell:

+
source ~/.zshrc
+
+
+

Creating a New Project

+

Run from the parent directory — the CLI creates the project folder for you:

+
cd ~/Downloads
+untoldengine-create create MyGame
+
+

Platform Options

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FlagTarget
--platform macosmacOS (default)
--platform iosiOS
--platform ios-ariOS with ARKit
--platform visionosvisionOS / Apple Vision Pro
--platform multimacOS + iOS + visionOS
+
# macOS project (default)
+untoldengine-create create MyGame --platform macos
+
+# iOS project
+untoldengine-create create MyGame --platform ios --bundle-id com.company.mygame
+
+# iOS with ARKit
+untoldengine-create create ARGame --platform ios-ar --bundle-id com.company.argame
+
+# visionOS / Apple Vision Pro
+untoldengine-create create VisionGame --platform visionos
+
+# Multi-platform (macOS, iOS, visionOS) — Team ID required for signing
+untoldengine-create create CrossGame --platform multi --team-id ABCD1234EF
+
+

All Options

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionDescriptionDefault
--platformTarget platformmacos
--bundle-idBundle identifier
--outputOutput directorycurrent directory
--macos-versionmacOS deployment target (13, 14, 15)15
--ios-versioniOS deployment target (16, 17, 18)17
--visionos-versionvisionOS deployment target (1, 2)2
--team-idApple Developer Team ID
--optimizationOptimization level (none, speed, size)none
--debug / --no-debugInclude debug informationyes
+
+

Updating an Existing Project

+

The update command refreshes only the GameData folder in an existing project, leaving your custom code untouched:

+
untoldengine-create update MyGame --asset-path ~/GameAssets
+
+# Or point to an absolute project path
+untoldengine-create update ~/Projects/MyGame --asset-path ~/GameAssets
+
+
+

Generated Project Structure

+
MyGame/
+├── Package.swift
+├── README.md
+└── Sources/
+    └── MyGame/
+        ├── AppDelegate.swift
+        ├── GameScene.swift
+        ├── GameViewController.swift
+        ├── Base.lproj/
+        │   └── Main.storyboard
+        ├── Info.plist
+        └── GameData/
+            ├── Scenes/
+            ├── Scripts/
+            ├── Models/
+            ├── Textures/
+            └── Shaders/
+
+

The starter GameScene.swift shows how to load a mesh, enable geometry streaming, and enable static batching.

+
+

Note: The demo references city.usdz. Place that file in GameData/Models/ before running.

+
+
+

Engine Dependencies by Platform

+

The generated Package.swift pulls in only the engine modules needed for your platform:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
PlatformEngine modules
macos / iosUntoldEngine
ios-arUntoldEngineAR
visionosUntoldEngineXR + UntoldEngineAR
multiUntoldEngine + UntoldEngineXR + UntoldEngineAR
+
+

Opening the Project

+

After create finishes, open the generated Xcode project:

+
open MyGame.xcodeproj
+
+

Select your scheme and press Run.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/Architecture/assetFormat/index.html b/site/Architecture/assetFormat/index.html new file mode 100644 index 00000000..37a12a85 --- /dev/null +++ b/site/Architecture/assetFormat/index.html @@ -0,0 +1,2640 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Asset Format - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

UntoldEngine Asset Format (.untold)

+

Overview

+

.untold is the native runtime asset container for UntoldEngine tile streaming.

+

It is not an interchange format and it is not intended to preserve full USD +semantics. USD/USDZ remains the authoring/import format. The .untold container is the +runtime package consumed by the engine for:

+
    +
  • tile files
  • +
  • per-tile LOD files
  • +
  • HLOD files
  • +
  • shared bucket files
  • +
+

V1 is intentionally narrow:

+
    +
  • static meshes only
  • +
  • transforms
  • +
  • bounds
  • +
  • vertex/index buffers
  • +
  • basic PBR materials
  • +
  • texture references
  • +
  • no animation
  • +
  • no skinning
  • +
  • no blend shapes
  • +
+

The design goals are:

+
    +
  • fast runtime parse with no ModelIO dependency
  • +
  • direct tile/HLOD/LOD streaming
  • +
  • byte-range-friendly remote streaming (tiles are downloaded on demand from HTTP/HTTPS CDNs and cached locally before parsing; see asset_remote_streaming.md)
  • +
  • explicit binary versioning
  • +
  • stable on-disk layout independent of Swift ABI
  • +
+

Core Rules

+
    +
  • Endianness: little-endian for all integers and floats
  • +
  • Float format: IEEE-754 Float32
  • +
  • Chunk alignment: 16-byte aligned file offsets
  • +
  • String encoding: UTF-8, null-terminated
  • +
  • Invalid reference sentinel: UInt32.max
  • +
  • Matrix encoding: 16 Float32s, column-major, matching simd_float4x4
  • +
  • AABB encoding: min.xyz followed by max.xyz
  • +
  • File offsets: absolute UInt64 offsets from the start of the file
  • +
  • Chunk-local offsets: UInt64 offsets from the start of the uncompressed chunk payload
  • +
  • Versioning: unsupported versions must be rejected by the loader
  • +
+

The Swift types in +Sources/UntoldEngine/AssetFormat/UntoldFormat.swift +are the logical schema. They are not the authoritative on-disk memory layout. +The exporter and loader must write/read fields explicitly.

+

Physical File Layout

+

Each .untold file is laid out as:

+
    +
  1. FileHeader
  2. +
  3. ChunkTable
  4. +
  5. aligned chunk payloads
  6. +
+

Recommended chunk payload order:

+
    +
  1. STRING_TABLE
  2. +
  3. ENTITY_TABLE
  4. +
  5. MESH_TABLE
  6. +
  7. MATERIAL_TABLE
  8. +
  9. TEXTURE_TABLE
  10. +
  11. VERTEX_DATA
  12. +
  13. INDEX_DATA
  14. +
+

This order allows the runtime to read metadata first and defer heavy geometry reads.

+

Header Encoding

+

The header is written field-by-field in this exact order:

+
magic[8]                     UInt8 x 8
+formatVersion                UInt32
+fileType                     UInt32
+flags                        UInt32
+headerSize                   UInt32
+chunkCount                   UInt32
+meshCount                    UInt32
+materialCount                UInt32
+textureRefCount              UInt32
+entityCount                  UInt32
+vertexLayout                 UInt32
+reserved0                    UInt32
+worldBounds.min              Float32 x 3
+worldBounds.max              Float32 x 3
+rootTransform                Float32 x 16
+contentHash                  UInt8 x 32
+reserved1                    UInt8 x 32
+
+

Rules:

+
    +
  • magic must be exactly 8 bytes, recommended value: "UNTOLD\0\0"
  • +
  • headerSize is the serialized byte size of the header, not MemoryLayout
  • +
  • contentHash is exactly 32 bytes
  • +
  • reserved1 is exactly 32 bytes
  • +
+

Chunk Table Encoding

+

Each chunk entry is written in this exact order:

+
chunkType                    UInt32
+compressionType              UInt32
+fileOffset                   UInt64
+compressedSize               UInt64
+uncompressedSize             UInt64
+elementCount                 UInt32
+reserved0                    UInt32
+
+

Rules:

+
    +
  • fileOffset must be 16-byte aligned
  • +
  • if compression is none, compressedSize == uncompressedSize
  • +
  • elementCount is used for record-table chunks
  • +
+

String Table Encoding

+

The string table payload is a raw byte blob containing:

+
    +
  • concatenated UTF-8 strings
  • +
  • one 0x00 terminator after each string
  • +
+

Rules:

+
    +
  • all string references are UInt32 byte offsets into this chunk
  • +
  • UInt32.max means “no string”
  • +
  • the exporter should deduplicate strings
  • +
  • the loader must verify that each referenced offset is within the chunk and reaches a null terminator before chunk end
  • +
+

Entity Record Encoding

+

Each entity record is serialized in this exact order:

+
entityId                     UInt32
+parentEntityId               UInt32
+nameOffset                   UInt32
+firstMeshRecordIndex         UInt32
+meshRecordCount              UInt32
+flags                        UInt32
+localBounds.min              Float32 x 3
+localBounds.max              Float32 x 3
+worldBounds.min              Float32 x 3
+worldBounds.max              Float32 x 3
+localTransform               Float32 x 16
+
+

Rules:

+
    +
  • parentEntityId == UInt32.max means no parent
  • +
  • mesh records for one entity should be contiguous in the mesh table
  • +
  • firstMeshRecordIndex + meshRecordCount must stay within mesh table bounds
  • +
+

Mesh Record Encoding

+

Each mesh record is serialized in this exact order:

+
entityId                     UInt32
+meshNameOffset               UInt32
+materialIndex                UInt32
+indexType                    UInt32
+vertexCount                  UInt32
+indexCount                   UInt32
+vertexStrideBytes            UInt32
+flags                        UInt32
+vertexDataOffset             UInt64
+indexDataOffset              UInt64
+vertexDataSizeBytes          UInt64
+indexDataSizeBytes           UInt64
+estimatedGPUBytes            UInt64
+reserved0                    UInt64
+localBounds.min              Float32 x 3
+localBounds.max              Float32 x 3
+
+

Rules:

+
    +
  • vertexDataOffset is relative to the start of VERTEX_DATA
  • +
  • indexDataOffset is relative to the start of INDEX_DATA
  • +
  • vertexDataSizeBytes == vertexCount * vertexStrideBytes
  • +
  • indexDataSizeBytes == indexCount * indexElementSize
  • +
  • materialIndex == UInt32.max means no material
  • +
+

Material Record Encoding

+

Each material record is serialized in this exact order:

+
nameOffset                        UInt32
+flags                             UInt32
+baseColorFactor                   Float32 x 4
+emissiveFactor                    Float32 x 3
+normalScale                       Float32
+metallicFactor                    Float32
+roughnessFactor                   Float32
+occlusionStrength                 Float32
+alphaCutoff                       Float32
+baseColorTextureIndex             UInt32
+normalTextureIndex                UInt32
+metallicTextureIndex              UInt32
+roughnessTextureIndex             UInt32
+emissiveTextureIndex              UInt32
+occlusionTextureIndex             UInt32
+reserved0                         UInt32 x 2
+
+

Rules:

+
    +
  • texture indices point into TEXTURE_TABLE
  • +
  • any texture index may be UInt32.max
  • +
  • flags holds alpha mode, double-sided, transparent, and similar runtime bits
  • +
+

Texture Reference Encoding

+

Each texture record is serialized in this exact order:

+
nameOffset                    UInt32
+uriOffset                     UInt32
+textureFormat                 UInt32
+flags                         UInt32
+width                         UInt32
+height                        UInt32
+mipCount                      UInt32
+reserved0                     UInt32
+
+

Rules:

+
    +
  • uriOffset points into the string table
  • +
  • the URI should reference a cooked texture asset or runtime-resolvable texture path
  • +
  • textureFormat describes the cooked/runtime texture format
  • +
+

Vertex Layout V1

+

V1 supports one layout only:

+
    +
  • UNT_VERTEX_LAYOUT_PBR_STATIC_V1
  • +
+

Serialized layout:

+
position.x                    Float32
+position.y                    Float32
+position.z                    Float32
+normalPacked                  UInt32
+tangentPacked                 UInt32
+uv0.u                         UInt16
+uv0.v                         UInt16
+uv1.u                         UInt16
+uv1.v                         UInt16
+color0.r                      UInt8
+color0.g                      UInt8
+color0.b                      UInt8
+color0.a                      UInt8
+
+

Total size: 32 bytes

+

Rules:

+
    +
  • uv0 is required
  • +
  • uv1 may be zeroed if unused
  • +
  • color0 defaults to 255,255,255,255 if unused
  • +
+

Packed Normal and Tangent Encoding

+

normalPacked and tangentPacked use signed normalized 10:10:10:2 packing.

+

For normalPacked:

+
    +
  • X: signed normalized 10-bit
  • +
  • Y: signed normalized 10-bit
  • +
  • Z: signed normalized 10-bit
  • +
  • W: unused, stored as 0
  • +
+

For tangentPacked:

+
    +
  • X: signed normalized 10-bit tangent x
  • +
  • Y: signed normalized 10-bit tangent y
  • +
  • Z: signed normalized 10-bit tangent z
  • +
  • W: handedness sign encoded in signed normalized 2-bit space
  • +
+

Recommended tangent handedness mapping:

+
    +
  • +1 for non-negative handedness
  • +
  • -1 for negative handedness
  • +
+

Exporter rules:

+
    +
  • normalize input normal/tangent before packing
  • +
  • clamp components to [-1, 1]
  • +
  • write normalPacked.w = 0
  • +
  • write tangentPacked.w = +1 or -1
  • +
+

Runtime rule:

+
    +
  • reconstruct bitangent as cross(normal, tangent.xyz) * tangentSign
  • +
+

Index Data Rules

+
    +
  • indexType = uint16 means 2 bytes per index
  • +
  • indexType = uint32 means 4 bytes per index
  • +
  • all indices in one mesh must use one type
  • +
  • exporter should prefer uint16 when vertexCount <= 65535
  • +
+

Compression Rules

+

Supported compression types:

+
    +
  • none
  • +
  • lz4
  • +
  • zstd
  • +
+

Rules:

+
    +
  • compression is applied per chunk, not whole-file
  • +
  • offsets stored in metadata reference the uncompressed chunk payload layout
  • +
  • metadata chunks may remain uncompressed for simpler startup
  • +
  • geometry chunks may be compressed
  • +
+

Validation Rules

+

The loader must reject files when:

+
    +
  • magic is invalid
  • +
  • version is unsupported
  • +
  • required chunks are missing
  • +
  • chunk offsets exceed file length
  • +
  • chunk offsets are not 16-byte aligned
  • +
  • string offsets fall outside the string table
  • +
  • mesh/entity/material/texture indices are out of range
  • +
  • vertex or index ranges exceed their chunk bounds
  • +
  • vertexStrideBytes does not match the declared vertex layout
  • +
  • indexDataSizeBytes does not match indexCount * indexElementSize
  • +
+

Implementation Notes

+

Do not serialize .untold files using MemoryLayout<T> or direct struct dumps.

+

Both the exporter and the loader must use explicit field-by-field helpers:

+
    +
  • writeUInt32LE
  • +
  • writeUInt64LE
  • +
  • writeFloat32LE
  • +
  • writeBytes
  • +
  • readUInt32LE
  • +
  • readUInt64LE
  • +
  • readFloat32LE
  • +
  • readBytes
  • +
+

This keeps the binary stable even if the Swift type layout changes.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/Architecture/assetProfiler/index.html b/site/Architecture/assetProfiler/index.html new file mode 100644 index 00000000..15d8d4cd --- /dev/null +++ b/site/Architecture/assetProfiler/index.html @@ -0,0 +1,2606 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Asset Profiler - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Asset Profiler

+

Purpose

+

AssetProfiler is a lightweight classification layer that runs between Mesh.parseAssetAsync() and ECS entity creation. It analyzes a parsed asset's composition — geometry bytes, texture bytes, mesh count — and recommends an AssetLoadingPolicy that selects the most appropriate residency strategy for each memory domain independently.

+

Its job is to answer: given this asset and this platform's memory budget, should geometry stream or load eagerly, and should textures stream or load eagerly?

+
+

Where It Fits

+
setEntityMeshAsync(.auto)
+  │
+  ├─ Mesh.parseAssetAsync()         ← CPU-only parse, no GPU allocation
+  │       └─ ProgressiveAssetData   ← MDLMesh objects in CPU RAM
+  │
+  ├─ AssetProfiler.profile()        ← analyze ProgressiveAssetData
+  │       └─ AssetProfile           ← geo bytes, tex bytes, mesh count, character
+  │
+  ├─ AssetProfiler.classifyPolicy() ← compare profile against platform budget
+  │       └─ AssetLoadingPolicy     ← geometryPolicy + texturePolicy
+  │
+  └─ Routing decision
+        geometryPolicy == .streaming → out-of-core stubs path
+        geometryPolicy == .eager     → immediate GPU upload path
+
+

The profiler runs entirely on the CPU in the async registration task. No GPU resources are allocated during classification.

+
+

AssetProfile

+
struct AssetProfile {
+    let totalFileBytes: Int
+    let estimatedGeometryBytes: Int   // vertex + index bytes, summed across all MDLMesh objects
+    let estimatedTextureBytes: Int    // estimated GPU footprint after decompression
+    let meshCount: Int
+    let materialCount: Int
+    let largestSingleMeshBytes: Int
+    let isEffectivelyMonolithic: Bool // meshCount <= 2
+    let assetCharacter: AssetCharacter
+}
+
+

AssetCharacter

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ValueMeaning
.textureDominatedTextures > 75% of combined estimate. Few or small meshes with large maps.
.geometryDominatedGeometry > 75% of combined estimate. Many or large meshes with minimal textures.
.mixedNeither domain exceeds 75%. Both contribute meaningfully.
.monolithic≤ 2 meshes. Streaming still prevents OOM at registration, but the mesh loads in one step rather than incrementally.
+
+

Geometry Byte Estimation

+

Geometry bytes are estimated using the same formula as CPUMeshEntry.estimatedGPUBytes — this keeps the profiler and the budget manager consistent:

+
vertexBytes  = vertexCount × vertexDescriptor.stride   (stride default: 48 bytes)
+indexBytes   = vertexCount × 3 × 4                     (~3 indices/vertex, 4 bytes each)
+meshBytes    = vertexBytes + indexBytes
+
+

Summed across all MDLMesh leaves in ProgressiveAssetData.topLevelObjects.

+
+

Texture Byte Estimation

+

Texture bytes are estimated without decompressing any texture data. The profiler scans MDLMaterial semantic slots for texture URL references and uses file sizes as a proxy:

+

For regular file URLs (external textures): +

textureBytes += fileSize × 3    // PNG/JPEG decode expansion; conservative cross-format estimate
+

+

For USDZ-embedded textures (bracket-notation paths like file:///scene.usdz[0/tex.png]): +Individual zip entries cannot be statted without decompressing. A file-level heuristic is used instead: +

packedTextureBytes = max(0, fileSizeBytes − (geometryBytes / 10))
+textureBytes = packedTextureBytes × 3
+
+The geometry division by 10 approximates the compression ratio of vertex/index data in the USDZ package. This overestimates for geometry-heavy assets, which is intentional — it is always safer to over-estimate texture cost (triggers streaming) than to under-estimate it (causes OOM).

+

If no texture URLs are found, estimatedTextureBytes is 0 and texturePolicy defaults to .eager.

+

Scanned Material Semantics

+
.baseColor, .roughness, .metallic, .bump, .emission, .opacity, .ambientOcclusion
+
+
+

AssetLoadingPolicy

+
struct AssetLoadingPolicy {
+    var geometryPolicy: GeometryResidencyPolicy  // .eager or .streaming
+    var texturePolicy:  TextureResidencyPolicy   // .eager or .streaming
+    var source:         PolicySource             // .auto or .userForced
+}
+
+

The two policies are independent. A texture-dominated asset with 3 meshes and 150 MB of maps gets geometry: .eager, texture: .streaming. A geometry-dominated city with 400 small meshes gets geometry: .streaming, texture: .eager.

+

Built-in Presets

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PresetGeometryTexture
.fullLoad.eager.eager
.geometryStreaming.streaming.eager
.textureStreaming.eager.streaming
.combinedStreaming.streaming.streaming
+
+

Classification Logic

+

Geometry Policy

+
if isMonolithic:
+    streaming if geometryBytes / budget > 0.30
+    else eager
+
+if meshCount >= 50:
+    streaming   ← many meshes spike GPU allocation regardless of total size
+
+if geometryBytes / budget > 0.30:
+    streaming
+
+else:
+    eager
+
+

Texture Policy

+
if textureBytes / budget > 0.10 OR textureBytes > 32 MB:
+    streaming
+
+else:
+    eager
+
+

Why fractions of budget, not fixed thresholds

+

The same 200 MB asset routes differently depending on the device:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DeviceBudgetGeo fractionGeometry policy
macOS1 GB20%.eager — fits comfortably
iOS high-end512 MB39%.streaming — too large
iOS low-end256 MB78%.streaming — far too large
visionOS512 MB39%.streaming — too large
+

Fixed thresholds (the old fileSizeThresholdBytes = 50 MB) applied the same cutoff on all platforms. A 200 MB asset would always trigger streaming, even on a macOS workstation with 1 GB of headroom.

+
+

Log Output

+

For every .auto classification the profiler emits two log lines:

+
[AssetProfiler] 'dungeon3' (2.1 MB) → mixed | geo ~2.9 MB, tex ~6.2 MB | budget: 1024 MB | meshes: 410
+[AssetProfiler] Policy → geometry: streaming, texture: eager (source: auto)
+
+

Line 1 — profile snapshot: filename, file size, asset character, estimated geometry bytes, estimated texture bytes, platform budget, mesh count.

+

Line 2 — the chosen policy for each domain and whether it was auto-selected or user-forced.

+

If geometry streaming is selected, the out-of-core log also captures the reason:

+
[OutOfCore] 'dungeon3': mixed asset, geo ~2.9 MB on 1024 MB budget → out-of-core stub registration (410 stubs)
+
+
+

Limitations and Known Heuristics

+

Texture estimation for USDZ-embedded textures is approximate. The (fileSizeBytes − geometryBytes/10) × 3 heuristic overestimates for geometry-heavy assets and underestimates for assets with highly compressed textures (e.g. ASTC at 8:1). It is biased toward overestimation intentionally.

+

Monolithic assets stream but do not incrementally load. An asset with 1 mesh and 400 MB of geometry enters the streaming path (to prevent OOM at registration) but loads its full geometry in a single upload step. There is no incremental benefit beyond registration-time safety.

+

External texture files must be accessible at classification time. If textures are on a remote URL or behind a slow filesystem, the stat() calls in estimateTextureBytes will block the registration task. This is only a concern for non-local assets.

+

Material semantics scanned are limited to the seven standard PBR slots. Custom material properties outside those semantics are not counted. If an asset uses non-standard semantic names, its texture estimate will be lower than the true cost.

+
+

Relationship to Other Systems

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SystemRelationship
ProgressiveAssetLoaderProvides ProgressiveAssetData that AssetProfiler.profile() analyzes
MemoryBudgetManagerProvides meshBudget; all thresholds are fractions of this value
GeometryStreamingSystemActivated when geometryPolicy == .streaming; manages GPU residency per entity
TextureStreamingSystemRuns on all entities with RenderComponent regardless of texture policy; the policy makes the intent explicit for future per-entity gating
RegistrationSystemCalls AssetProfiler in the .auto branch of setEntityMeshAsync; maps the result to useOutOfCore
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/Architecture/asset_remote_streaming/index.html b/site/Architecture/asset_remote_streaming/index.html new file mode 100644 index 00000000..fd0f45a1 --- /dev/null +++ b/site/Architecture/asset_remote_streaming/index.html @@ -0,0 +1,2965 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Asset Remote Streaming - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Asset Remote Streaming

+

Overview

+

UntoldEngine supports streaming scene geometry directly from remote HTTPS servers (e.g., a CDN). Plain http:// URLs are rejected at the engine boundary to prevent unencrypted asset transmission. When a scene manifest URL points to a remote HTTPS server, the engine downloads the manifest, resolves all tile/HLOD/LOD asset URLs relative to that server, and downloads each asset on demand as the camera approaches. Downloaded assets are stored in a persistent disk cache so subsequent sessions never re-download unchanged files.

+

This layer sits below the tile streaming system but above the local asset loading pipeline. The rest of the engine — GeometryStreamingSystem, TileComponent, UntoldReader — is unaware of whether an asset came from disk or the network; RemoteAssetDownloader resolves a remote URL to a local cached path before the file is opened.

+
+

System Components

+

RemoteAssetDownloader (actor)

+

Sources/UntoldEngine/Systems/RemoteAssetDownloader.swift

+

The single download actor for all remote assets. Responsibilities:

+
    +
  • Downloads assets via URLSession and commits them to AssetDiskCache.
  • +
  • HTTPS-only enforcementlocalURL(for:) throws DownloadError.insecureScheme immediately for any non-HTTPS URL. Plain http:// is never sent to the network.
  • +
  • Single-flight deduplication — if two tiles request the same URL concurrently, only one network request is issued. The second caller suspends until the first completes, then returns the cached path.
  • +
  • Exponential backoff retry — up to 3 attempts, with delays of 1 s, 2 s, and 4 s between attempts.
  • +
  • Conditional GET — if a cached ETag sidecar exists for a URL, the next request includes If-None-Match: <etag>. A 304 Not Modified response returns the cached path instantly without re-downloading.
  • +
  • Texture pre-fetch — after downloading a .untold file, immediately fetches all textures referenced in its texture table in the background so they are cache-resident before the tile is parsed.
  • +
+

Public API:

+
func localURL(for remoteURL: URL) async throws -> URL
+
+

Returns a local file:// URL pointing to the cached asset. Throws on permanent failure after all retries are exhausted.

+

URLSession configuration:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterValue
timeoutIntervalForRequest30 s
timeoutIntervalForResource300 s
Max retry attempts3 (delays: 1 s, 2 s, 4 s)
Retry delay formula2^attempt seconds
+
+

AssetDiskCache (actor)

+

Sources/UntoldEngine/Systems/AssetDiskCache.swift

+

A persistent LRU cache that maps remote URLs to local files on disk.

+
    +
  • Content-addressed storage — the cache key is SHA256(url.absoluteString). Files are stored at <cacheDir>/<hash>.<ext>.
  • +
  • ETag sidecars — ETags from server responses are stored in <hash>.meta (JSON). Retrieved by RemoteAssetDownloader for conditional GET on subsequent requests.
  • +
  • Atomic writes — each file is written to a temp path first, then renamed. A crash during download leaves an orphaned temp file, not a corrupt cache entry.
  • +
  • LRU eviction — when total cache usage exceeds the budget (default 500 MB), the cache evicts entries by lastAccess timestamp (oldest first) until usage falls to 75% of budget.
  • +
  • Texture sub-paths — textures are stored at relative paths under the cache root via storeAtRelativePath(_:data:), so NativeFormatLoader can resolve texture URIs by the same relative path they have inside the .untold file.
  • +
+

Cache parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterDefault
Cache directoryLibrary/Caches/UntoldAssetCache/
Budget500 MB
Eviction target75% of budget
Cache keySHA256(url.absoluteString)
ETag storage<hash>.meta
Write strategytmp file → atomic rename
+
+

URL Resolution

+

The engine resolves asset URLs lazily at load time. The helper resolveAssetURL(_:label:) is called by the tile loading path before opening any file:

+
func resolveAssetURL(_ url: URL, label: String) async -> URL? {
+    if url.scheme?.lowercased() == "http" {
+        // Plain HTTP is rejected — only HTTPS is permitted.
+        let error = RemoteAssetDownloader.DownloadError.insecureScheme("http")
+        Logger.logError(message: "[TileStreaming] Remote download failed for \(label): \(error)")
+        return nil
+    }
+    guard url.scheme?.lowercased() == "https" else {
+        return url  // Local file:// path — pass through unchanged
+    }
+    do {
+        return try await RemoteAssetDownloader.shared.localURL(for: url)
+    } catch {
+        Logger.logError(message: "[TileStreaming] Remote download failed for \(label): \(error)")
+        return nil
+    }
+}
+
+

Local file:// paths bypass the downloader entirely. Only https:// URLs go through the remote download cache. Plain http:// URLs are explicitly rejected with a logged error and a nil return, which causes the tile load to mark the tile .failed and enter exponential backoff.

+
+

Tile URL Construction

+

When setEntityStreamScene(entityId:url:) is called with a remote manifest URL, tile asset URLs are resolved relative to the manifest directory:

+
Manifest:    https://cdn.example.com/dungeon3/dungeon3.json
+Tile path:   tiles/tile_0_0.untold
+→ Tile URL:  https://cdn.example.com/dungeon3/tiles/tile_0_0.untold
+
+HLOD path:   hlods/tile_0_0_hlod.untold
+→ HLOD URL:  https://cdn.example.com/dungeon3/hlods/tile_0_0_hlod.untold
+
+

The same construction applies to HLOD and per-tile LOD URLs. All of these are stored in their respective TileComponent fields as absolute https:// URLs. resolveAssetURL translates them to cached local paths at load time.

+
+

End-to-End Flow

+

Phase 1 — Manifest download

+
let sceneRoot = createEntity()
+setEntityName(entityId: sceneRoot, name: "scene")
+
+if let manifestURL = URL(string: "https://cdn.example.com/scene/scene.json") {
+    setEntityStreamScene(entityId: sceneRoot, url: manifestURL)
+}
+
+
  │
+  ├─ URL has https scheme → download manifest
+  │   └─ RemoteAssetDownloader.localURL(for: manifestURL)
+  │       ├─ AssetDiskCache.localURL(for:) → cache miss
+  │       ├─ URLSession GET with optional If-None-Match header
+  │       ├─ 200 OK → AssetDiskCache.store(data:for:etag:) atomically
+  │       └─ Return local file:// path
+  │
+  └─ Decode JSON at local path → TileManifest
+     └─ registerTiledScene(manifest:baseURL:)
+         ├─ Construct tile URLs relative to manifest URL
+         ├─ Register lightweight TileComponent stubs (no geometry)
+         └─ Each stub stores tileURL as https:// in TileComponent
+
+

Phase 2 — Tile download (on demand, per streaming tick)

+
GeometryStreamingSystem: camera enters prefetchRadius for tile_0_0
+  │
+  └─ loadTile(entityId:)
+      ├─ setEntityMeshAsync(entityId: meshEntity, ...)
+      │
+      └─ Inside async Task:
+          ├─ resolveAssetURL(tileURL)          ← tileURL is https://
+          │   └─ RemoteAssetDownloader.localURL(for: tileURL)
+          │       ├─ Cache hit? → return local path immediately
+          │       └─ Cache miss? → download, store, return local path
+          │
+          ├─ Parse asset at local path
+          │   ├─ .untold → UntoldReader (no ModelIO dependency)
+          │   └─ .usdz/.usdc → ModelIO via NativeFormatLoader
+          │
+          └─ Upload geometry to Metal GPU buffers
+
+

Phase 3 — Texture pre-fetch (for .untold assets)

+

After a .untold file is downloaded, RemoteAssetDownloader immediately pre-fetches all referenced textures in the background:

+
RemoteAssetDownloader downloads tile_0_0.untold
+  │
+  └─ downloadTextures(in: untoldData, remoteBaseURL: tileDirectoryURL)
+      ├─ Parse texture table URIs from .untold header
+      └─ For each texture URI (e.g. "Textures/wall_albedo.png"):
+          ├─ Check AssetDiskCache for relative path → skip if present
+          ├─ Construct: remoteBaseURL + "/" + uri
+          ├─ Download → AssetDiskCache.storeAtRelativePath(uri, data:)
+          └─ (Failures skipped; geometry still loads, just untextured)
+
+

Textures are stored at their relative URI under the cache root. When NativeFormatLoader later resolves texture paths, it finds them at the same relative path without knowing they came from a CDN.

+
+

Caching Mechanics

+

Cache Hit (steady state)

+

On the second session or after an initial warm-up pass, all tiles are cached locally:

+
resolveAssetURL(tileURL)
+  └─ RemoteAssetDownloader.localURL(for: tileURL)
+      └─ AssetDiskCache.localURL(for: tileURL) → hit
+          └─ Return file:// path instantly (zero network I/O)
+
+

Manifest Revalidation (ETag)

+

On subsequent sessions the manifest is revalidated cheaply:

+
GET /scene/scene.json
+Headers: If-None-Match: "abc123"
+
+← 304 Not Modified
+   → Return cached manifest path, no re-download
+   → All tile URLs resolved from unchanged local manifest
+
+

If the server returns 200 with a new ETag, the manifest and its ETag sidecar are updated atomically.

+

LRU Eviction

+

When accumulated downloads exceed the 500 MB budget:

+
AssetDiskCache.evictToLimit()
+  ├─ Sort entries by lastAccess ascending (oldest first)
+  └─ Delete files (and their .meta sidecars) until usage ≤ 375 MB (75%)
+
+

Evicted files are re-downloaded transparently on next access. The cache directory persists across app launches; only explicit clearCache() or OS-driven cache purges remove it entirely.

+
+

Retry and Error Handling

+

Network retry (RemoteAssetDownloader)

+ + + + + + + + + + + + + + + + + + + + + + + + + +
AttemptDelay before next attempt
0— (immediate first try)
11 s
22 s
3 (final)4 s
+

After 3 failed attempts, localURL(for:) throws. The tile load marks state .failed and enters the tile-level exponential backoff (5 s → 10 s → 20 s → 60 s max) before retrying.

+

HTTP 304 Not Modified

+

Treated as a cache hit. No file write occurs; localURL(for:) returns the existing cached path.

+

HTTP error codes (4xx, 5xx)

+

Counted as a failure, subject to the retry policy above. A 404 is retried like any other error — the manifest may be temporarily unavailable or behind an eventual-consistent CDN.

+

Texture pre-fetch failures

+

Individual texture download failures are silently skipped. Geometry still loads and renders, just without that texture (Metal will bind a default 1×1 white fallback). This prevents a single bad texture URL from blocking tile geometry.

+

Single-flight deduplication

+

If 10 tiles all reference the same shared texture and all request it simultaneously, only one URLSession task fires. The other 9 suspend and resume with the cached result once the first download completes.

+
+

Integration with Tile Streaming

+

Remote streaming is transparent to GeometryStreamingSystem and TileComponent. From their perspective:

+
    +
  • TileComponent.tileURL holds the canonical URL for the tile (https:// for remote scenes, file:// for local scenes).
  • +
  • loadTile() calls setEntityMeshAsync, which calls resolveAssetURL to get a local path before parsing.
  • +
  • The streaming state machine (.unloaded → .parsing → .parsed → .unloading) is identical regardless of whether the asset was local or remote.
  • +
  • Tile retry backoff, grace-period unloads, memory budget gates, and prefetch radius behavior all apply equally to remote tiles.
  • +
+

The only difference in behavior is a latency bump on first load when a tile is not yet cached. The prefetch radius absorbs most of this: the tile begins downloading when the camera is effectivePrefetchRadius away, giving the download time to complete before the camera reaches streamingRadius.

+

For a typical scene with streamingRadius = 80 m and unloadRadius = 120 m, effectivePrefetchRadius auto-computes to 100 m. At walking speed (~1.5 m/s) this is ~13 seconds of headroom — well above the download + parse time for a 15–20 MB tile over a 10 Mbps connection (~12–16 s worst case).

+
+

Demo Game Configuration

+

Sources/DemoGame/DemoState.swift registers two remote scenes:

+
let remoteScenes: [RemoteSceneOption] = [
+    .init(
+        id: "dungeon",
+        title: "Dungeon",
+        manifestURL: URL(string: "https://cdn.example.net/dungeon3/dungeon3.json")!
+    ),
+    .init(
+        id: "city",
+        title: "City",
+        manifestURL: URL(string: "https://cdn.example.net/city/city.json")!
+    ),
+]
+
+

GameScene.loadTileScene(url:) creates a root entity and passes it with the manifest URL to setEntityStreamScene(entityId:url:):

+
func loadTileScene(sceneID: String, url: URL, completion: @escaping @Sendable (Bool) -> Void) {
+    clearSceneBatches()
+    GeometryStreamingSystem.shared.enabled = true
+
+    let sceneRoot = createEntity()
+    setEntityName(entityId: sceneRoot, name: sceneID)
+
+    setEntityStreamScene(entityId: sceneRoot, url: url) { success in
+        completion(success)
+    }
+}
+
+

The HUD (DemoHUD.swift) presents scene options; on selection it calls onLoadTiledScene which routes to loadTileScene(url:).

+
+

Threading and Safety

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ConcernMechanism
RemoteAssetDownloader actor isolationSwift actor — safe to call from any Task or thread
AssetDiskCache actor isolationSwift actor — all reads and writes are serialised
Single-flight gateinFlightDownloads: [URL: Task<URL, Error>] dictionary, actor-protected
Atomic cache writesTemp file + FileManager.moveItem (atomic on same volume)
ECS mutationsMain thread only, via withWorldMutationGate
Tile completion guardscene.exists(entityId) checked before every ECS write in upload completion closures
+
+

Key Design Parameters Summary

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterValueLocation
Permitted URL schemeshttps:// only — http:// is rejectedRemoteAssetDownloader, resolveAssetURL
Max download retries3RemoteAssetDownloader
Retry delay formula2^attempt secondsRemoteAssetDownloader
Request timeout30 sURLSession configuration
Resource timeout300 sURLSession configuration
Disk cache budget500 MBAssetDiskCache
Cache eviction target75% of budgetAssetDiskCache
Cache keySHA256(url.absoluteString)AssetDiskCache
ETag revalidationYes (conditional GET)RemoteAssetDownloader
Texture pre-fetchYes (post-download, async)RemoteAssetDownloader
Single-flight dedupYes (actor-isolated dictionary)RemoteAssetDownloader
+
+

See Also

+ + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/Architecture/batchingSystem/index.html b/site/Architecture/batchingSystem/index.html new file mode 100644 index 00000000..ae309b0b --- /dev/null +++ b/site/Architecture/batchingSystem/index.html @@ -0,0 +1,2464 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Batching System - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

BatchingSystem — How It Works

+

The goal is simple: instead of issuing 100 separate draw calls (one per entity), merge entities that share the same material into a single combined GPU buffer and issue one draw call per material group. This is called static batching.

+
+

Step 0: The World is Divided into Cells

+

The 3D world is partitioned into a 3D grid of cells. The cell size is calibrated at scene load time: when a tile manifest is present, the cell size is set to 2 × tileSize so that cell boundaries align with tile boundaries. When no manifest is present, the default of 32 world units is used.

+

Every entity is assigned to a cell based on the world-space center of its bounding box:

+
cellId(x, y, z) = floor(worldCenter / cellSize)
+
+

Why align to tile boundaries? Batching 100 entities scattered across a huge world into one mesh is wasteful — you'd rebuild everything when anything changes. Cells localize the damage. When cell boundaries align with tile boundaries, loading or unloading a tile only touches the cells that tile occupies — no cross-tile batch rebuilds.

+
+

Step 1: Entity Registration

+

When your 100 entities load, each one that has a StaticBatchComponent gets registered:

+
    +
  • Eligibility check (resolveBatchCandidate): the entity must have a RenderComponent, WorldTransformComponent, no skeleton/animation, no transparency, no gizmo/light component, and its mesh must already be resident in memory. The LOD index is derived from LODComponent.currentLOD (entity-level LOD), then TileLODTagComponent.levelIndex (per-tile LOD/HLOD children), defaulting to 0. isLODBatch on the resulting BatchGroup is true if any member entity has either component.
  • +
  • If eligible → it gets assigned to a cell and added to cellToEntities[cellId].
  • +
  • The cell is marked dirty and its state becomes renderableUnbatched.
  • +
+
+

Step 2: Per-Frame Tick — The Pipeline

+

Every frame, tick() runs through this pipeline:

+

2a. Process Removals & Additions

+

Any entities that changed (LOD switch, mesh evicted/streamed in) are removed from their old cell and re-registered in their current cell. This marks the affected cells dirty.

+

Tile-loaded entities bypass the quiescence delay. When a fullLoad tile (or LOD/HLOD load) finishes, GeometryStreamingSystem calls BatchingSystem.notifyTileEntitiesResident(_:) with the set of render-ready entity IDs. This single call directly registers the entities in pendingEntityAdditions, marks them as tile-parsed (for quiescence bypass), and resolves their cell membership — replacing the former two-step queueResidencyEventsForRenderDescendants + notifyTileParsedEntities pairing and avoiding the per-entity event storm through SystemEventBus. Their cells are immediately promoted to batchPending in the same tick. See Tile-Local Batch Promotion below.

+

Stale entity purge on LOD/HLOD teardown. When unloadLODLevel or unloadHLOD destroys child entities, it first calls BatchingSystem.cancelPendingEntities(_:) with the render descendant IDs. This removes them from pendingEntityAdditions, pendingEntityRemovals, newlyResidentEntities, and tileParsedEntityIds before the entities are destroyed — preventing "entity is missing" errors on the next tick() and avoiding wasted batch rebuilds for entities that no longer exist.

+

2b. Update Visibility History

+

The system checks which cells currently contain visible entities and records cellLastVisibleFrame[cellId]. This drives visibility gating — the system won't waste CPU rebuilding cells you can't see.

+

2c. Promote Dirty Cells → batchPending

+

For each dirty cell in state renderableUnbatched or streaming: +- Is it visible (or recently visible within 120 frames)? +- Has it been stable for at least quiescenceFramesBeforeBatchBuild frames (default: 1)?

+

If yes → state becomes batchPending.

+

Cells flagged by tile promotion skip the quiescence check and advance directly to batchPending in the same tick.

+

2d. Rebuild Dirty Cells (rebuildDirtyCells)

+

This is the core build loop:

+
    +
  1. +

    Apply completed background artifacts first — results from previous frames' async builds are swapped in (up to maxArtifactAppliesPerTick = 4 per frame).

    +
  2. +
  3. +

    Gather batchPending cells and build rebuild candidates. For each:

    +
  4. +
  5. Estimate the work: count total vertices + indices + bytes across all entities in the cell.
  6. +
  7. If a cell exceeds the per-cell complexity guard (>160K verts, >300K indices, >8MB), it's flagged runtimeIneligibleCells and stays unbatched.
  8. +
  9. +

    Otherwise it becomes a CellRebuildCandidate.

    +
  10. +
  11. +

    Sort candidates by priority:

    +
  12. +
  13. Currently visible > recently visible > long ago visible
  14. +
  15. Smaller estimated bytes first (lighter work first)
  16. +
  17. +

    Oldest dirty-since-frame first

    +
  18. +
  19. +

    Apply per-tick budgets: up to 8 cells, 120K verts, 220K indices, 6MB total per tick. Once budgets are exhausted, remaining cells defer to next frame.

    +
  20. +
  21. +

    Snapshot build inputs under a world mutation gate: for each selected cell, group its entities' meshes by BatchBuildKey = (cellId, materialHash, lodIndex). This produces CellBuildInput.

    +
  22. +
  23. +

    Dispatch background builds on artifactBuildQueue (a .utility DispatchQueue). The heavy work — actually merging vertex data — happens off the main thread.

    +
  24. +
+
+

Step 3: Building the Batch (buildPreparedArtifact)

+

For each CellBuildInput, on the background thread:

+
    +
  • Iterate material groups. Skip any group with < 2 meshes (no point batching a single mesh).
  • +
  • For each group that qualifies, call createBatchGroup:
  • +
  • Loop through all meshes in the group.
  • +
  • For each mesh, extract positions, normals, UVs, tangents from the Metal buffers.
  • +
  • Transform each vertex by the entity's world transform (worldTransform.space * mesh.localSpace).
  • +
  • Re-index indices with an offset (since vertices are now concatenated into one flat array).
  • +
  • Compute the world-space AABB of the merged geometry (min/max of all vertex positions).
  • +
  • Allocate new MTLBuffers for the merged position/normal/UV/tangent/index data.
  • +
  • The result is a PreparedCellArtifact containing [BatchGroup].
  • +
+

So 100 entities all sharing the same wood-plank material → 1 BatchGroup with 1 merged MTLBuffer and one tight world-space AABB.

+
+

What is an artifact? +An artifact is the output package produced by a build job: + the input is CellBuildInput (a snapshot of which entities are in a cell and how they're grouped by material), and the artifact (PreparedCellArtifact) is the finished result — the merged MTLBuffers, entity-to-batch mappings, vertex/index counts, and build time. Everything needed to install the batch into the live scene. +

+
+
+

Step 4: Applying the Artifact

+

Back on the main thread (next frame or same frame if sync mode):

+
    +
  • Validate the artifact is still current (epoch + generation check — discards stale builds if the scene changed while it was building).
  • +
  • Remove any existing batches for the cell (queue old GPU buffers for retirement with a 3-frame safety delay so the GPU isn't still using them).
  • +
  • Append the new BatchGroups to batchGroups.
  • +
  • Update entityToBatch[entityId] so the renderer knows each entity is now represented by a batch.
  • +
  • Reconcile streaming textures: if a texture streamed to a higher mip while the build was in flight, patch the batch's material in-place so it doesn't revert.
  • +
  • Mark cell state → renderableBatched.
  • +
+
+

Step 5: Rendering

+

The renderer uses cluster-level frustum culling to determine which batch groups to submit. Each BatchGroup carries a precomputed world-space AABB (boundingBox) covering all geometry in the group. The render passes test each group's AABB directly against the current-frame frustum — one AABB test per batch group, not one per entity. Groups whose AABB is fully outside the frustum are skipped without any entity-level traversal.

+

For batch groups that survive the AABB test, each BatchGroup is one draw call with its merged buffer. 100 entities sharing one material = 1 draw call, submitted only when the group's spatial bounds are within the frustum.

+

Per-entity batching membership (entityToBatch) is still maintained and used by non-batched rendering paths and by tests.

+
+

Step 6: Retirement (Safe GPU Buffer Release)

+

Old GPU buffers aren't freed immediately. They go into retiringBatchArtifacts with a retireAfterFrame = currentFrame + 3. After 3 frames, the system drops the Swift reference, allowing ARC to release the MTLBuffers — guaranteeing the GPU has finished with them.

+
+

Cell Lifecycle State Machine

+
unloaded
+   ↓ (entity becomes resident)
+streaming
+   ↓ (quiescence + visibility check pass)
+   ↓ (or: tile-local promotion — bypasses quiescence)
+renderableUnbatched
+   ↓ (promoted, budget available)
+batchPending
+   ↓ (build dispatched + applied)
+renderableBatched
+   ↓ (entity removed or LOD change)
+retiring → unloaded
+
+
+

Tile-Local Batch Promotion

+

When a fullLoad tile (one that completes GPU upload in a single step, occCount == 0), per-tile LOD level, or HLOD mesh finishes loading, GeometryStreamingSystem hands off the set of render-ready entity IDs to the batching system:

+
BatchingSystem.shared.notifyTileEntitiesResident(renderIds)
+
+

This single call combines what was previously a per-entity AssetResidencyChangedEvent storm + a separate notifyTileParsedEntities call. The entities are registered directly in the batching system's pending additions and marked for quiescence bypass, avoiding hundreds of individual events through SystemEventBus.

+

In the next tick(), those entities are processed differently from ordinary streaming arrivals:

+
    +
  1. deferBatchBuild = false — the extra one-frame deferral applied to newly-resident streaming entities is suppressed.
  2. +
  3. Immediate cell promotion — after the entities are registered, the system collects the cells they were assigned to and forces them from renderableUnbatchedbatchPending in the same tick, bypassing the quiescenceFramesBeforeBatchBuild wait.
  4. +
+

The result: a tile that finishes parsing on frame N will have its cells in batchPending on frame N+1 and a completed batch ready to submit one or two frames later (depending on background build time), rather than waiting for the quiescence window first.

+

This is safe because a tile's geometry arrives atomically — all entities are registered at once with no further churn expected. The quiescence delay exists to absorb incremental arrivals (OCC stub uploads); it is unnecessary and harmful for the fullLoad tile path.

+

OCC tiles are unchanged. For tiles using the out-of-core upload path, individual mesh stubs still arrive one at a time via the normal handleResidencyChange flow. The quiescence delay is preserved for these so the batch doesn't rebuild for each individual stub upload.

+
+

BatchGroup AABB

+

Every BatchGroup stores a precomputed world-space AABB:

+
var boundingBox: (min: simd_float3, max: simd_float3)
+
+

This is computed during createBatchGroup as the min/max of all transformed vertex positions. It represents the tightest world-space bounding box over all geometry in the group.

+

The AABB serves two purposes: +1. Cluster-level frustum culling — the render pass tests this AABB against currentFrameFrustum before encoding the draw call, skipping entire groups that are outside the view. +2. Future use — HLOD transitions, occlusion culling, and GPU-driven rendering will all use this AABB as the cluster's spatial identity.

+
+

With 100 Entities — Concrete Example

+

Suppose your 100 entities break down as: +- 60 entities: wood material, LOD 0, all in cell (0,0,0) +- 30 entities: stone material, LOD 0, cell (0,0,0) +- 10 entities: glass material (transparent) → excluded from batching

+

Result: +- 2 BatchGroups for cell (0,0,0): one wood, one stone +- Each BatchGroup has its own world-space AABB covering all vertices in the group +- 2 draw calls instead of 90 (the 10 transparent ones draw individually) +- Both groups are frustum-culled by AABB before encoding — if the whole cell is off-screen, 0 draw calls are issued +- On a LOD change (say 20 wood entities switch to LOD 1), cell (0,0,0) is marked dirty → rebuild fires next eligible tick → now 3 BatchGroups (wood LOD0, wood LOD1, stone LOD0)

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/Architecture/geometryStreamingSystem/index.html b/site/Architecture/geometryStreamingSystem/index.html new file mode 100644 index 00000000..5ae598e5 --- /dev/null +++ b/site/Architecture/geometryStreamingSystem/index.html @@ -0,0 +1,2898 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Geometry Streaming System - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

GeometryStreamingSystem

+

The Setup: OCC Stubs Inside a Loaded Tile

+

Imagine a large tile has been parsed through setEntityStreamScene(...) and classified into the out-of-core path. The tile now contains many internally created OCC child stubs — building_A, building_B, streetlamp_01, car_01, etc. Each stub has a StreamingComponent that stores:

+
    +
  • assetFilename / assetExtension — where the mesh or source asset came from
  • +
  • streamingRadius — how close the camera must be to load it
  • +
  • unloadRadius — how far before it gets unloaded
  • +
  • priority — which buildings load first when slots are contested
  • +
  • state.unloaded, .loading, .loaded, or .unloading
  • +
+
+

Every Frame: update(cameraPosition:deltaTime:)

+

The engine calls this every frame. Here's what happens:

+

1. Throttle Check

+

The system normally does real work every 0.1 seconds (updateInterval). Between ticks, it's a no-op. This prevents wasting CPU every single frame.

+

When lastPendingLoadBacklog > 0 (candidates are queued but all slots are busy), the effective interval drops to burstTickInterval (default 16 ms). This prevents a 100 ms stall between slot pickups during active loading. The tick rate returns to 100 ms once the backlog drains.

+

OS pressure bypass — if a pendingPressureRelief flag is set (fired by the OS pressure callback on a background queue), the throttle check is bypassed entirely for that call. This guarantees eviction runs within one frame (≤ 11 ms at 90 fps) rather than waiting up to 100 ms for the next normal tick. Without this, a .critical signal arriving right after a tick would sit unprocessed for the full throttle interval — longer than visionOS's kill window.

+

2. Spatial Query via Octree

+

Instead of checking every OCC child stub in the scene, it asks the OctreeSystem:

+
+

"Give me every entity within 500m of the camera."

+
+

This is the key performance trick — only nearby entities are evaluated.

+

3. Classify Each Nearby Entity (lines 129–157)

+

For each entity the octree returns, the system calculates the distance from camera to the entity's bounding box center, then:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StateConditionAction
.unloadeddistance ≤ streamingRadius→ add to load candidates
.loadeddistance > unloadRadius→ add to unload candidates
.loadedstill in range→ stamp lastVisibleFrame (keep alive)
.loading / .unloadingskip, already in progress
+

4. Out-of-Range Loaded Entities (lines 164–183)

+

The octree query only covers nearby space. But what if building_Z was loaded and the player sprinted far away — it might not be in the octree result anymore. So the system also checks its loadedStreamingEntities tracking set for any loaded entity not in the octree result, and adds those to unload candidates if they're too far.

+
+

Unloading First: Free Memory Before Loading New Things (lines 191–197)

+

Unload candidates are sorted farthest-first (most wasteful memory first). Up to maxUnloadsPerUpdate = 12 are processed per tick to avoid frame spikes.

+

unloadMesh() does: +1. Sets state → .unloading +2. Notifies BatchingSystem the entity is retiring +3. Cancels any in-flight load task +4. Calls MeshResourceManager.shared.release(entityId:) — decrements reference count on the cached mesh +5. Clears render.mesh = [] — the GPU buffers are not destroyed (cache still owns them) +6. Clears LOD level meshes if applicable +7. Unregisters from MemoryBudgetManager +8. Sets state → .unloaded +9. Fires an AssetResidencyChangedEvent(isResident: false)

+
+

Loading: Async, Concurrency-Limited

+

Load candidates are sorted by priority then distance (high priority + closest first). Only maxConcurrentLoads = 3 can be active simultaneously.

+

Before dispatching, the scheduler applies four guards in order:

+
    +
  1. Tile ownership (isTileOwned) — the entity must be a descendant of a TileComponent entity. Non-tile-owned entities are rejected immediately and their state is never mutated. StreamingComponent is an internal, tile-subordinate mechanism; it is not valid on standalone entities. See StreamingComponent Ownership Model below.
  2. +
  3. CPU-entry readiness — OOC entities whose CPUMeshEntry is not yet stored in ProgressiveAssetLoader are skipped. This prevents pre-streaming stubs from holding slots while registration is still running.
  4. +
  5. Prewarm-active deferral — entities for roots whose background texture prewarm is still running are skipped. Dispatching while the prewarm holds the per-asset texture lock would block all concurrent slots for the remaining prewarm duration. Slots stay free until isPrewarmActive returns false.
  6. +
  7. Per-candidate geometry budget check — if the candidate's estimated GPU footprint would exceed the geometry budget, evictLRU is called first.
  8. +
+

When all near-band candidates share one assetRootEntityId, the near-band concurrency limit expands from nearBandMaxConcurrentLoads to maxConcurrentLoads. All sub-meshes of one USDZ are treated as a single burst rather than being serialized one-at-a-time.

+

loadMesh() does: +1. Reserves a slot in activeLoads (thread-safe via NSLock) +2. Sets state → .loading +3. Notifies BatchingSystem streaming started +4. Spawns a Swift Task (runs off the main thread)

+

Inside the async task: +- If the entity has a LODComponent → calls reloadLODEntity() which loads all LOD levels +- Otherwise → calls loadMeshAsync() which goes to MeshResourceManager (cache-first, file fallback) +- After loading, back on the main thread via withWorldMutationGate: + - Assigns render.mesh with fresh copies of uniform buffers (critical — prevents entities sharing GPU state from overwriting each other) + - Sets state → .loaded + - Fires AssetResidencyChangedEvent(isResident: true) + - Records load in MemoryBudgetManager

+
+

StreamingComponent Ownership Model

+

StreamingComponent is an internal, tile-subordinate component. It is not a public API for external callers.

+
    +
  • Only entities that are descendants of a TileComponent entity may have an active StreamingComponent. loadMesh() enforces this with the isTileOwned() guard, which walks the ScenegraphComponent.parent chain and returns false for any entity that has no TileComponent ancestor.
  • +
  • StreamingComponent stubs are created internally by setEntityMeshAsync for the out-of-core (OCC) path when a tile file is large enough to be split into sub-mesh stubs. External callers never attach StreamingComponent directly.
  • +
  • enableStreaming() is internal; it is not part of the public API.
  • +
+

What to use instead

+ + + + + + + + + + + + + + + + + + + + + +
Use caseAPI
Streamable geometry exported by the Blender pipeline (terrain, city blocks, large scenes)setEntityStreamScene(entityId:url:completion:) with a local or remote (https://) manifest URL
Handcrafted streaming zones (dungeon rooms, level sectors, indoor areas)StreamingRegionManager — register explicit StreamingRegion AABB + asset lists; no manifest required
Always-resident objects (characters, props, HUD elements)setEntityMeshAsync(entityId:filename:withExtension:completion:)
+

setEntityStreamScene is the preferred public entry point for streamable scene geometry. It accepts a root EntityID and a local file:// path or remote https:// URL. For remote URLs it downloads and caches the manifest via RemoteAssetDownloader before decoding. It then calls the internal registerTiledScene() and hands off all streaming lifecycle management to GeometryStreamingSystem. The backwards-compatible loadTiledScene(manifest:) / loadTiledScene(url:) overloads remain available and create an internal root entity automatically. See asset_remote_streaming.md for the full remote download lifecycle.

+
isTileOwned(entityId:) — private helper in GeometryStreamingSystem+MeshStreaming.swift
+  Walks ScenegraphComponent.parent chain upward.
+  Returns true only if a TileComponent is found somewhere in the ancestry.
+  Returns false for any standalone entity (no parent chain, or chain ends without a tile).
+
+
+

LOD Path: reloadLODEntity() (lines 313–415)

+

For LOD entities (e.g., a skyscraper with 3 detail levels), it: +1. Loads all LOD levels concurrently from cache/disk +2. Calculates current camera-to-entity distance +3. Picks the appropriate LOD level (highest detail that fits distance) +4. Sets renderComponent.mesh to that LOD's mesh data +5. Marks lodComponent.currentLOD

+
+

Memory Pressure: Texture Relief First, Geometry Eviction Last

+

The engine uses two independent memory pressure signals and responds to them in priority order:

+ + + + + + + + + + + + + + + + + + + + +
Pressure signalMethodMeaning
CombinedshouldEvict()Geometry pool ≥ 85% of geometryBudget OR texture pool ≥ 85% of textureBudget
Geometry onlyshouldEvictGeometry()Mesh allocations alone ≥ 85% of geometryBudget
+

Why two signals? TextureStreamingSystem upgrades visible textures to higher resolutions after meshes load. Those upgrades increase totalTextureMemory in MemoryBudgetManager. If the load gate used the combined signal, texture upgrades on already-loaded meshes would silently prevent new mesh loads — even when the geometry-only footprint is well within budget. The split pools (geometryBudget + textureBudget) ensure each domain has an independent ceiling so neither can starve the other.

+

Step 1 — Texture downgrade relief

+

Before considering geometry eviction, the system sheds texture quality on the farthest loaded entities:

+
if combined pressure is high AND geometry pressure is NOT high:
+    TextureStreamingSystem.shedTextureMemory(maxEntities: 4)
+    → no geometry eviction; texture relief only
+
+

shedTextureMemory forces the farthest entities in the upgradedEntities set to minimumTextureDimension immediately, bypassing the normal distance-band schedule. A distant wall dropping from 1024 px to 256 px is far less noticeable than a missing mesh.

+

Step 2 — Geometry eviction (last resort)

+

Only triggered when geometry memory itself hits the high-water mark:

+
if geometry pressure is high:
+    TextureStreamingSystem.shedTextureMemory(maxEntities: 8)   ← try texture relief first
+    evictLRU(cameraPosition:)                                  ← then fall back to geometry eviction
+
+

evictLRU: +1. First evicts unused cached meshes (MeshResourceManager.evictUnused()) +2. Collects all loaded streaming entities +3. Sorts by value score (far + large = first to go; see value-score eviction in the out-of-core walkthrough) +4. Unloads them one by one until geometry-only pressure clears (loop breaks on shouldEvictGeometry(), not the combined signal) +5. Skips entities that are both visible and within visibleEvictionProtectionRadius (30 m default) +6. Accepts an optional maxEvictions cap (default Int.max). The OS pressure path passes 16 per call — this bounds single-frame work during a burst. Any remaining candidates spill to subsequent ticks.

+

The sizeFactor in the eviction score is normalized against geometryBudget (not the combined budget), so a mesh consuming 80% of the geometry pool scores correctly rather than appearing to consume only ~48% of a combined total.

+

Step 3 — OS memory pressure (proactive, out-of-band)

+

In addition to the per-tick budget checks above, MemoryBudgetManager subscribes to OS memory pressure events via DispatchSource.makeMemoryPressureSource:

+ + + + + + + + + + + + + + + + + + + + +
OS signalResponsemaxEntities
.warningTexture shed8
.criticalTexture shed + double geometry eviction pass (capped at 16 per pass) + CPU heap release20
+

The OS callback fires on a background queue and sets a pendingPressureRelief flag on GeometryStreamingSystem. The flag is drained at the start of the next update() tick on the main thread, so all eviction work stays on the same thread as the rest of the streaming system. This prevents the OS from silently escalating to .critical and terminating the process — on visionOS in particular, the window between .warning and process kill can be under a second.

+

CPU heap release on critical pressureevictLRU only frees GPU Metal buffers tracked by MemoryBudgetManager. The OS measures total process memory, which includes ProgressiveAssetLoader.rootAssetRefs (the live MDLAsset tree and all child CPUMeshEntry vertex/index buffers). For a 500-building scene this CPU heap can reach hundreds of megabytes. On .critical, after the two geometry eviction passes, GeometryStreamingSystem calls ProgressiveAssetLoader.shared.releaseWarmAsset(rootEntityId:) on every warm root. This frees the CPU heap immediately. The rehydration context (asset URL + loading policy) is retained, so a cold re-stream from disk is transparent when the camera re-approaches.

+
+

City Block Scenario: Summary Flow

+
Player spawns at corner of city block
+│
+├─ Frame 1 tick: Octree finds 8 nearby buildings
+│   ├─ 5 are unloaded + within streamingRadius → load candidates
+│   └─ 3 are loading already → skip
+│
+├─ Up to 3 async loads fire simultaneously
+│   ├─ building_A: cache miss → read from USDZ file
+│   ├─ building_B: cache hit → instant
+│   └─ building_C: cache miss → read from USDZ file
+│
+├─ Player walks forward → building_K enters range
+│   └─ Queued in load candidates (backlog until a slot frees)
+│
+├─ Player runs past old buildings → building_A now > unloadRadius
+│   └─ render.mesh cleared, reference released, memory freed
+│
+└─ Memory pressure → LRU eviction kicks in
+    └─ building_E (not visible, oldest lastVisibleFrame) → evicted
+
+

The key design decisions here are: +- Octree spatial query prevents O(n) entity iteration every tick +- Concurrency cap (3) prevents GPU/IO saturation during fast movement +- Adaptive tick rate — 16 ms during backlog, 100 ms steady-state — prevents stalls between slot pickups without wasting CPU when idle +- Single-root burst detection — when all near-band candidates are sub-meshes of one asset, concurrency expands to the global cap so the asset loads in parallel rather than one mesh at a time +- Background texture prewarmloadTextures() runs at registration time so the first-upload path is a no-op and lock wait ≈ 0 +- Prewarm-active deferral — dispatch is held until the prewarm releases the texture lock, keeping all slots free for the burst +- Narrowed texture lock scope — the per-asset lock covers only ensureTexturesLoaded; makeMeshesFromCPUBuffers runs outside the lock so all slots upload in parallel +- CPU-entry readiness guard — stubs registered before their CPU data is ready are skipped rather than wasting a slot +- Unload-before-load ordering ensures you free memory before consuming more +- Cache ownership means unloading just clears references, actual GPU memory is reused if the same mesh comes back into range +- Geometry-only load gate prevents texture upgrades from blocking mesh loads — each domain is budgeted independently +- Texture relief before geometry eviction means a drop in distant texture resolution is always preferred over a missing mesh +- Split geometry/texture pools (geometryBudget + textureBudget) give each domain an independent ceiling and high-water mark — a texture-heavy scene cannot crowd out geometry loads and vice versa +- Runtime device budget probinggeometryBudget and textureBudget are derived at init from MTLDevice.recommendedMaxWorkingSetSize (macOS) or os_proc_available_memory() (visionOS/iOS) rather than hardcoded platform defaults; budgets adapt to actual device headroom +- SceneRootTransform consistency — all distance calculations (GeometryStreamingSystem, LODSystem, inline LOD upload helpers) pass camera position through SceneRootTransform.shared.effectiveCameraPosition() so XR physical-head movement and scene-root translations are applied uniformly; raw cameraComponent.localPosition is never used directly for distance math +- Camera sync always runssyncStreamingCameraPosition() executes every frame regardless of the loading flag; decoupling it from the loading guard prevents the streaming camera from freezing while an asset load is in flight +- OS memory pressure subscriptionDispatchSource.makeMemoryPressureSource fires proactive texture shedding and geometry eviction before the OS escalates to process termination; the response runs on the next update() tick to stay single-threaded +- evictLRU per-call cap — the maxEvictions parameter (default Int.max) bounds single-frame eviction work; the OS pressure path uses 16 per pass so a .critical burst doesn't spike one frame; remaining candidates spill to subsequent ticks +- CPU heap release on critical pressure — on .critical, after geometry eviction, ProgressiveAssetLoader.releaseWarmAsset() is called for every warm root, freeing the MDLAsset CPU heap the OS measures; rehydration context survives so cold re-stream from disk is transparent

+
+

Tile-Level Streaming

+

The tile-level streaming layer sits above the mesh-level OOC streaming. For full documentation, architecture diagrams, and tuning guidance see docs/Architecture/tilebasedstreaming.md. This section summarises the key design decisions that interlock with the mesh-level system documented above.

+

Per-frame passes in update() (tile layer, in order)

+
    +
  1. Tile load pass — dispatches .unloaded stubs within effectivePrefetchRadius (frustum-gated, budget-gated, up to maxConcurrentTileLoads). Tiles with LOD levels are gated: the full tile only loads when the camera is within the finest LOD switch distance.
  2. +
  3. Tile unload pass — three sub-passes (nearby beyond unloadRadius, .parsed outside query radius, .parsing outside query radius).
  4. +
  5. HLOD streaming pass — for each tile stub, loads or unloads the HLOD proxy based on camera distance vs hlodSwitchDistance and tileComp.state. Uses hlodHysteresisFactor (default 0.90) to prevent thrashing at boundaries. Capped by maxConcurrentHLODLoads (default 4).
  6. +
  7. HLOD out-of-range cleanup — unloads HLOD entities for tiles that drifted entirely outside maxQueryRadius.
  8. +
  9. Per-tile LOD streaming pass — for each tile stub, finds the target LOD level for the current distance with hysteresis (lodHysteresisFactor, default 0.90). Skips when HLOD is resident (avoids dual representation). Loads target; unloads others; unloads all when tile is .parsed. Capped by maxConcurrentLODLoads (default 4).
  10. +
  11. Per-tile LOD out-of-range cleanup — unloads LOD entities for tiles outside maxQueryRadius.
  12. +
+

TileComponent

+

Tile stubs carry a TileComponent (no StreamingComponent, no RenderComponent):

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldPurpose
tileURLAbsolute URL of the tile asset — file:// for local scenes, https:// for remote CDN scenes
fileSizeBytesPre-computed file size for the memory budget gate
streamingRadiusVisual display threshold — tile is rendered once parsed
prefetchRadiusBackground load threshold (> streamingRadius); auto = midpoint of stream/unload gap
unloadRadiusDistance beyond which teardown is scheduled
priorityLoad-order priority when multiple tiles are candidates
tileIdDebug identifier matching the manifest tile_id
state.unloaded → .parsing → .parsed → .unloading
pendingUnloadSinceCFAbsoluteTime when tile first exceeded unloadRadius; 0 = in range
loadTaskThe in-flight Swift Task (cancelled on teardown)
meshEntityIdThe dedicated mesh-child entity ID; stored so the asset-loading timeout guard can force-close AssetLoadingGate if loadTextures() hangs
hlodURLURL of the HLOD proxy USDC, if present in the manifest
hlodEntityIdECS entity holding the HLOD mesh; .invalid when unloaded
hlodStateHLOD lifecycle: .unloaded → .loading → .loaded → .unloading
hlodSwitchDistanceCamera distance beyond which the HLOD is shown
hlodLoadTaskIn-flight HLOD load Task (cancelled before hlodState = .unloading)
lodLevels[TileLODLevel] — per-tile intermediate LOD entries parsed from manifest
parseStartTimeCFAbsoluteTime set when tile CPU work begins (after remote fetch). The tile-parse watchdog uses this; stays 0 during remote download so the 60 s clock does not run on network latency
lastHLODTransitionTimeCFAbsoluteTime of the most recent HLOD load/unload; used by secondaryRepresentationMinDwellSeconds guard to prevent faster-than-1 s HLOD flip-flop
lastLODTransitionTimeCFAbsoluteTime of the most recent per-tile LOD load/unload; same dwell guard as HLOD
+

Tile Load Pass (inside update())

+

After the mesh-level scan, a second pass handles tile stubs:

+
    +
  1. For each .unloaded stub within effectivePrefetchRadius + 1.0 (not streamingRadius — tiles start loading before the camera enters the display zone):
  2. +
  3. Scores distance against the predictive (look-ahead) camera position.
  4. +
  5. Applies the frustum gate (tileStreamingFrustum, padded by tileFrustumGatePadding = 20 m).
  6. +
  7. Collects as a load candidate.
  8. +
  9. Geometry budget gate — if shouldEvictGeometry(), runs texture shedding and evictLRU (capped at 8) before dispatch.
  10. +
  11. Up to maxConcurrentTileLoads (default 2) dispatched via loadTile(), subject to the tileParseMemoryBudgetMB (200 MB) in-flight gate.
  12. +
+

Tile Unload Pass (inside update())

+

Three sub-passes each tick, capped at maxTileUnloadsPerUpdate (default 2) total teardowns:

+
    +
  1. Nearby tiles beyond unloadRadius — differentiates by state:
  2. +
  3. .parsing (not yet visible) — cancelled immediately, no grace delay.
  4. +
  5. .parsed (visible geometry) — starts or checks the unload grace period (unloadGracePeriod = 3 s). Tile only tears down after being out of range for the full grace period; the timer resets if the camera returns inside unloadRadius.
  6. +
  7. .parsed tiles outside the octree query radius — same grace period logic.
  8. +
  9. .parsing tiles outside the octree query radius — cancelled immediately to prevent ghost geometry flashes on fast movement or teleports.
  10. +
+

Design Decisions

+
    +
  • Prefetch radius decouples load from display — tiles start loading at effectivePrefetchRadius (auto: midpoint of stream/unload gap) so the parse completes before the camera reaches the visual zone, eliminating blank-screen pops on tile entry.
  • +
  • Grace period prevents oscillation — a 3-second hold on .parsed tile teardown stops rapid load/unload cycles at tile boundaries, which was the primary cause of flickering in large scenes.
  • +
  • maxTileUnloadsPerUpdate = 2 — spreading GPU buffer releases across frames prevents a single-frame blank when many tiles leave range simultaneously.
  • +
  • maxConcurrentTileLoads = 2 — two concurrent parses balance throughput for large scenes against RAM spike risk. Each parse calls MDLAsset(url:) on a full USDC file; the tileParseMemoryBudgetMB gate serialises naturally when a large tile saturates the budget.
  • +
  • blockRenderLoop: false on all tile/LOD/HLOD loadssetEntityMeshAsync is called with blockRenderLoop: false so that AssetLoadingGate.isLoadingAny is not held true during the (potentially multi-second) parse. Without this, concurrent parses freeze visibleEntityIds updates and stall the render loop.
  • +
  • Hysteresis on LOD/HLOD transitionslodHysteresisFactor (default 0.90) and hlodHysteresisFactor (default 0.90) add a 10% inner band so the camera must move meaningfully past a switch boundary before the current representation is unloaded. Without hysteresis, frame-to-frame distance jitter causes rapid load/unload cycles that freeze the engine.
  • +
  • cancelPendingEntities before entity destruction — when unloadLODLevel or unloadHLOD tears down child entities, it first calls BatchingSystem.shared.cancelPendingEntities(_:) with the render descendant IDs, purging them from all pending batching queues. This prevents "entity is missing" errors when the batching tick tries to process additions for entities that were destroyed between event queuing and tick processing.
  • +
  • notifyTileEntitiesResident replaces the event storm — tile/LOD/HLOD load completions call BatchingSystem.shared.notifyTileEntitiesResident(_:) instead of the former two-step queueResidencyEventsForRenderDescendants + notifyTileParsedEntities pairing. The single call directly registers entities in the batching system's pending additions and marks them for quiescence bypass, avoiding hundreds of individual AssetResidencyChangedEvent objects through SystemEventBus.
  • +
  • Identity world transform on stubs — tile geometry is exported in world space; no runtime coordinate conversion needed.
  • +
  • .auto streaming policy — tiles use the same admission gate as regular assets; unexpectedly large tiles are gracefully rejected and retried rather than crashing.
  • +
  • Zombie-state guard in completion — completion callback checks tc.state == .parsing. If unloadTile ran mid-parse (state is .unloading), result is discarded and the pre-created child entity is cleaned up — stub never enters a "geometry missing" zombie state.
  • +
  • defer slot releasereleaseActiveTileLoad in defer frees the concurrency slot on all exit paths (success, failure, cancelled-state early return).
  • +
  • removeTileComponent deregisters from streaming system — cancels in-flight loadTask and calls GeometryStreamingSystem.shared.unregisterTileEntity(entityId) to atomically remove the entity from all four tile tracking sets (loadedTileEntities, loadingTileEntities, activeTileLoads, meshEntityToTileEntity).
  • +
  • reset() clears tile tracking sets — called by setEntityStreamScene() (via registerTiledScene) to reset interiorZone and firstRangeTimestamps so stale scene-level state from the previous scene does not persist into the new scene's streaming passes.
  • +
  • HLOD unload racehlodState = .unloading is set before hlodLoadTask.cancel(). The load-completion callback checks hlodState before marking .loaded; setting it first ensures the callback always discards a racing in-flight result.
  • +
  • Per-tile LOD follows the same race fixlevel.state = .unloading is set before level.loadTask?.cancel().
  • +
  • LOD unload-all on tile parse — when loadTile's completion callback fires (state transitioning to .parsed), both unloadHLOD(entityId:) and unloadAllLODLevels(entityId:) are called. The full tile has taken over all distance bands; intermediate representations are no longer needed.
  • +
  • loadedLODEntities tracking set — mirrors loadedTileEntities for the LOD layer. Allows reset() to cancel all in-flight LOD tasks and setEntityStreamScene() second-call safety to clear stale LOD entity IDs.
  • +
  • AssetLoadingGate timeoutmeshEntityId is stored in TileComponent so the timeout guard can call AssetLoadingState.shared.finishLoading(entityId: meshEntityId) without an O(n) map scan if loadTextures() hangs and the gate would otherwise remain open permanently.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/Architecture/lodSystem/index.html b/site/Architecture/lodSystem/index.html new file mode 100644 index 00000000..6a16b84b --- /dev/null +++ b/site/Architecture/lodSystem/index.html @@ -0,0 +1,2399 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + LOD System - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

LOD in UntoldEngine: Two Independent Systems

+

UntoldEngine has two separate LOD mechanisms that operate at different granularities. Understanding the distinction is important before reading either system's details.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Entity-level LOD (this document)Per-tile LOD (tile streaming)
UnitIndividual mesh entity (LODComponent)Whole tile USDC file (TileLODLevel)
ControlLODSystem — runs every frameGeometryStreamingSystem.update() — runs per tick
Switch triggerCamera distance vs LODLevel.maxDistanceCamera distance vs TileLODLevel.switchDistance (with hysteresis)
Hysteresis5-unit inner band on finer-LOD transitions onlylodHysteresisFactor (default 0.90 = 10% band) on active level
Meshes in memoryAll LOD levels GPU-resident simultaneouslyOnly the active LOD level is loaded
Use caseIndividual detailed objects (buildings, props)Tile-granularity intermediate representations for large scenes
Content pipelineSeparate OBJ/USDZ per LOD level, wired via LODComponentSeparate USDC per tile LOD, listed in manifest lod_levels array
Debug taggingLODComponent.currentLOD read by LOD debug rendererTileLODTagComponent.levelIndex placed on render descendants
+

Per-tile LOD documentation: docs/Architecture/tilebasedstreaming.md.

+
+

Entity-level LOD: City Block with 500 Buildings, 3 LODs Each

+

Setup (before the system runs)

+

Each building entity has a LODComponent with an array of LODLevel entries sorted by detail:

+
Building Entity
+  LODComponent.lodLevels[0] → LOD0: highDetailMesh,  maxDistance: 50.0
+  LODComponent.lodLevels[1] → LOD1: medDetailMesh,   maxDistance: 100.0
+  LODComponent.lodLevels[2] → LOD2: lowDetailMesh,   maxDistance: 200.0
+
+

Each LODLevel tracks its own residencyState (.resident, .loading, .notResident, .unknown) and a url so the streaming system knows where to reload it from.

+
+

Every Frame: LODSystem.update()

+

The system runs once per frame. Here's what happens for all 500 buildings:

+

Step 1 — Get camera position +

CameraSystem → activeCamera → CameraComponent.localPosition
+

+

Step 2 — Query all LOD entities +

queryEntitiesWithComponentIds([LODComponent, WorldTransformComponent])
+
+Returns all 500 building entities in one shot.

+

Step 3 — For each building: updateEntityLOD()

+

This is the core loop. Three sub-steps per building:

+
+

3a. calculateDistance()

+

Takes the building's local AABB bounding box, finds its center, transforms it to world space via WorldTransformComponent.space, then computes the straight-line distance to the camera.

+
distance = |cameraPosition - worldCenter|
+
+
+

3b. selectLODLevel()

+

Applies lodBias to the distance (default 1.0, so no change), then walks through lodLevels in order, comparing against each level's maxDistance:

+
adjustedDistance = distance * lodBias
+
+If adjustedDistance ≤ 50   → desiredLOD = 0  (high detail)
+If adjustedDistance ≤ 100  → desiredLOD = 1  (medium)
+If adjustedDistance ≤ 200  → desiredLOD = 2  (low)
+Beyond all thresholds      → desiredLOD = 2  (lowest available)
+
+

Hysteresis: When switching to a finer LOD (e.g. camera approaching, LOD2→LOD1), the threshold is tightened by 5.0 units. This prevents flickering when the camera hovers right at a boundary. Switching to coarser LODs has no penalty — it happens immediately.

+
+

3c. resolveActualLOD()

+

The desired LOD may not be resident yet (e.g. it's still streaming in). So:

+
Is desiredLOD mesh resident?
+  YES → use it (isUsingFallback = false)
+  NO  → findFallbackLOD():
+          Try coarser LODs first  (LOD2, LOD3...)
+          Then try finer LODs     (LOD0...)
+          If nothing → stay at currentLOD
+
+

This means a building 30m away that wants LOD0 but hasn't finished loading will temporarily show LOD1 or LOD2 — always something visible, never a pop-in hole.

+
+

3d. applyLOD()

+

This is the write step. Runs inside withWorldMutationGate to be thread-safe.

+
If newLOD == currentLOD and no transition in progress → skip (no-op)
+
+Otherwise:
+  - Update lodComponent.currentLOD = newLOD
+  - Copy lodLevels[newLOD].mesh → renderComponent.mesh
+  - Generate meshAssetID: "<url>_LOD<index>"
+  - Store in lodComponent.activeMeshAssetID  (used by batching system)
+  - If LOD actually changed → emit EntityLODChangedEvent to SystemEventBus
+
+

The renderComponent.mesh swap is the handoff to the renderer — whatever mesh array sits there is what gets drawn next frame.

+
+

What the 500-Building Frame Looks Like

+

Given a camera standing near one end of the block:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DistanceBuildingsDesired LODTypical Outcome
0–50m~20LOD0High detail meshes
50–100m~80LOD1Medium meshes
100–200m~150LOD2Low meshes
200m+~250LOD2 (last)Lowest available
+

The system processes all 500 in sequence, but buildings where LOD hasn't changed are early-exited with no mutation (the newLOD == previousLODIndex guard). In a stable scene, the vast majority of buildings hit that fast path.

+
+

Key Design Observations

+
    +
  • Fallback-first streaming: The system never waits for a mesh to load — it always degrades gracefully to whatever is resident.
  • +
  • activeMeshAssetID is the bridge to the batching system. When a LOD switches, the new asset ID tells the batcher to move that entity into a different batch group.
  • +
  • EntityLODChangedEvent on SystemEventBus is how downstream systems (geometry streaming, batching) learn that a switch happened — they react to the event rather than polling.
  • +
  • Fade transitions exist in the code (transitionProgress, previousLOD) but enableFadeTransitions defaults to false, so currently all switches are instant.
  • +
+
+

LOD + Out-of-Core Integration

+

Prior to this integration, LOD and OOC were mutually exclusive: assets that qualified for out-of-core streaming would bypass LOD group detection, causing each LOD0/LOD1/LOD2 object to become an independent stub entity.

+

How it works now:

+

setEntityMeshAsync runs LOD group detection before the OOC branching decision. When the asset both qualifies for OOC streaming and contains LOD groups, the LOD+OOC path runs:

+
    +
  1. +

    Registration — one entity per LOD group (not one per MDLObject). Each entity gets a LODComponent with stub LODLevels (empty mesh, .notResident) and a StreamingComponent(.unloaded).

    +
  2. +
  3. +

    CPU registryProgressiveAssetLoader.cpuLODRegistry[groupEntityId][lodIndex] stores a CPUMeshEntry for every LOD level. The MDLAsset is retained so CPU buffers remain valid.

    +
  4. +
  5. +

    GPU upload — when GeometryStreamingSystem picks up the entity (.unloaded → in streaming range), it calls uploadActiveLODFromCPU. This uploads all LOD levels in one pass from the CPU registry, marks each LODLevel.residencyState = .resident, and sets renderComponent.mesh to the level appropriate for the current camera distance.

    +
  6. +
  7. +

    LOD switching after loadLODSystem.applyLOD continues to work as normal: it reads from lodComponent.lodLevels[n].mesh (now populated) and swaps renderComponent.mesh. No additional streaming requests are needed — all levels are already GPU-resident.

    +
  8. +
  9. +

    Cold re-hydration — if releaseWarmAsset was called on the root and a group entity re-enters streaming range, rehydrateColdAsset re-parses the USDZ, re-runs LOD detection, and rebuilds cpuLODRegistry entries before uploadActiveLODFromCPU runs.

    +
  10. +
+

Result: the caller sets any MeshStreamingPolicy and LOD assets always get proper LODComponent wiring. The mutual exclusivity that required users to choose between OOC and LOD is eliminated.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/Architecture/meshResourceManager/index.html b/site/Architecture/meshResourceManager/index.html new file mode 100644 index 00000000..c9093508 --- /dev/null +++ b/site/Architecture/meshResourceManager/index.html @@ -0,0 +1,2330 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Mesh Resource Manager - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

MeshResourceManager

+

MeshResourceManager is a singleton that acts as the shared GPU memory layer for mesh assets. It caches entire USDZ files, tracks which entities use which meshes via reference counting, and evicts unused geometry under memory pressure.

+
+

The Core Data Model

+

MeshResourceManager is a singleton (shared) that manages two key dictionaries:

+
resources:      URL -> MeshResource       (the cache — one entry per USDZ file)
+entityToMesh:   EntityID -> (URL, name)   (which entity uses which mesh)
+
+

A MeshResource holds all meshes from a single USDZ file, keyed by asset name:

+
city_block_A.usdz  ->  MeshResource {
+    meshesByName:    { "building_01": [...], "window_01": [...], "door_01": [...] }
+    refCountByName:  { "building_01": 12, "window_01": 48, "door_01": 12 }
+    totalMemorySize: 42_000_000   // bytes on GPU
+    lastAccessFrame: 1042
+}
+
+
+

Phase 1 — Loading (First Request)

+

Say entity e001 needs "building_01" from city_block_A.usdz:

+
loadMesh(url: city_block_A.usdz, meshName: "building_01")
+
+

Step 1 — Cache check (getCachedMesh): nothing cached yet → miss.

+

Step 2 — Single-flight gate (waitForExistingLoadOrBecomeLoader): +- First caller wins: it is designated the loader and gets true. +- If 10 other entities request the same file simultaneously, they all queue up as waiters inside inFlightLoadWaiters[url]. They suspend via CheckedContinuation — no busy-waiting.

+

Step 3 — Parse & upload (Mesh.loadSceneMeshesAsync): +- The entire USDZ is parsed once. Every mesh in the file is uploaded to GPU buffers. +- Result is a [[Mesh]] — one inner array per named asset.

+

Step 4 — Build the dictionary and cache it: +

meshesByName["building_01"] = [mesh1, mesh2, ...]   // LODs or submeshes
+meshesByName["window_01"]   = [mesh3, ...]
+meshesByName["door_01"]     = [mesh4, ...]
+totalMemorySize = 42 MB
+
+This is stored in resources[city_block_A.usdz].

+

Step 5 — Wake waiters (finishInFlightLoad): +- All 10 suspended callers are resumed with false (they don't load — file is already cached). +- Each then calls getCachedMesh and gets their mesh instantly.

+
+

Phase 2 — Reference Counting (Entity Lifecycle)

+

When the streaming system decides entity e001 will render "building_01":

+
retain(url: city_block_A.usdz, meshName: "building_01", for: e001)
+
+

This: +1. Releases any previous mesh the entity held (safety cleanup). +2. Records entityToMesh[e001] = (city_block_A.usdz, "building_01"). +3. Increments refCountByName["building_01"] from 0 → 1.

+

After 500 entities are assigned across the city block:

+
refCountByName: { "building_01": 45, "window_01": 180, "streetlight": 60, ... }
+totalRefCount = 500
+isInUse = true   ← cannot be evicted
+
+

When an entity scrolls out of view and is culled:

+
release(entityId: e099)
+
+

This removes entityToMesh[e099] and decrements refCountByName["building_01"] from 45 → 44.

+
+

Phase 3 — Eviction (Memory Pressure)

+

Three eviction strategies exist:

+ + + + + + + + + + + + + + + + + + + + + +
MethodWhen to Use
evict(url:)Force-remove one specific file (only if refCount == 0)
evictUnused()Sweep all files with zero references
evictToFreeMemory(targetBytes:)LRU — evict oldest-accessed files first until targetBytes freed
+

For a city block, evictToFreeMemory is most useful. Say GPU budget is exceeded by 80 MB:

+
candidates = resources where totalRefCount == 0
+           sorted by lastAccessFrame ascending  (oldest first)
+
+Evict city_block_C.usdz  → frees 38 MB   (last seen frame 200)
+Evict city_block_D.usdz  → frees 44 MB   (last seen frame 310)
+Total freed: 82 MB ✓
+
+

For each evicted mesh, mesh.cleanUp() is called to free the Metal GPU buffers.

+

lastAccessFrame is updated every time a mesh is accessed (cache hit) or retained, so actively-used files naturally survive LRU pressure.

+
+

Thread Safety

+

All state is protected by a concurrent DispatchQueue with the readers-writers pattern: +- Reads use accessQueue.sync — concurrent reads are fine. +- Writes use accessQueue.sync(flags: .barrier) — exclusive access, blocks concurrent reads.

+

This makes the manager safe to call from multiple streaming tasks loading different USDZ files in parallel.

+
+

Summary Flow for 500-Mesh City Block

+
Frame 0: Scene starts loading
+  → 5 USDZ files queued
+  → Each file: one task becomes loader, others wait
+  → All 5 files parsed, all meshes cached in GPU memory
+
+Frame 1–N: Entities stream in/out of view
+  → retain() as entities enter view frustum
+  → release() as entities leave
+  → refCounts track exactly how many entities use each mesh
+
+Memory pressure detected:
+  → evictToFreeMemory() walks LRU list
+  → Only files with refCount==0 are eligible
+  → GPU buffers freed via cleanUp()
+  → Files still in view are untouched
+
+

The key insight: one disk parse per USDZ file, no matter how many entities share its meshes. Reference counting prevents premature eviction, and LRU ensures the least-recently-seen geometry is freed first under memory pressure.

+
+

Callers

+

MeshResourceManager is used by three systems:

+

GeometryStreamingSystem — Primary driver

+

Owns the full lifecycle: +- Updates currentFrame each tick to keep LRU timestamps fresh. +- Calls loadMesh + retain when an entity streams into view. +- Calls release when an entity streams out of view. +- Calls evictUnused() to free GPU memory. +- Reads getStats() for diagnostics and memory budgeting.

+

RegistrationSystem — Cache pre-warmer

+

Calls cacheLoadedMeshes(url:meshArrays:) when entities are registered into the scene, so meshes are already in the cache before the streaming system requests them. Also calls release when entities are unregistered.

+

UntoldEngine (Renderer) — Read-only monitoring

+

Reads getStats() only, likely for a debug overlay or performance HUD.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/Architecture/outOfCore/index.html b/site/Architecture/outOfCore/index.html new file mode 100644 index 00000000..e81c0d86 --- /dev/null +++ b/site/Architecture/outOfCore/index.html @@ -0,0 +1,2391 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Out-of-Core Geometry - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Out-of-Core Geometry in the Current Architecture

+

Overview

+

UntoldEngine still uses an out-of-core (OOC) geometry path, but it is now part of the tile streaming architecture, not a general public workflow for arbitrary standalone entities.

+

The public entry point for streamed geometry is:

+
setEntityStreamScene(entityId: sceneRoot, manifest: "city")
+
+

Inside that pipeline, large tiles may be classified as OOC during setEntityMeshAsync(streamingPolicy: .auto).

+

The Three Runtime Roles

+ + + + + + + + + + + + + + + + + + + + + +
SystemResponsibility
RegistrationSystemParses the tile payload and chooses fullLoad vs OOC registration
ProgressiveAssetLoaderStores CPU-resident CPUMeshEntry records and warm/cold rehydration context
GeometryStreamingSystemUploads and evicts tile-owned OCC stubs by distance and budget
+

MeshResourceManager still serves disk/cache-backed mesh loads for non-OOC paths, but OOC residency is fundamentally driven by ProgressiveAssetLoader.

+

What OOC Means Now

+

When a large tile is routed to the OOC path:

+
    +
  1. The tile payload is parsed on the CPU.
  2. +
  3. Child stub entities are created with StreamingComponent(state: .unloaded).
  4. +
  5. Their CPU mesh data is stored in ProgressiveAssetLoader.
  6. +
  7. GeometryStreamingSystem uploads those stubs incrementally as the camera approaches.
  8. +
  9. Evicted stubs can re-upload from the warm CPU registry without reparsing the file.
  10. +
+

Those StreamingComponent stubs are valid only when they are descendants of a TileComponent entity. GeometryStreamingSystem.loadMesh(...) enforces that ownership rule.

+

Public API Boundary

+

The runtime still exposes MeshStreamingPolicy, but the architecture has moved:

+
    +
  • setEntityMeshAsync(..., streamingPolicy: .auto) is fine for normal always-resident assets
  • +
  • setEntityMeshAsync(..., streamingPolicy: .immediate) forces full upload
  • +
  • setEntityStreamScene(...) is the supported public streaming path
  • +
  • StreamingComponent and enableStreaming(...) are internal tile/OOC mechanisms
  • +
+

That means the old pattern:

+
setEntityMeshAsync(..., streamingPolicy: .outOfCore)
+enableStreaming(...)
+
+

is no longer the recommended app-level workflow. The engine now expects streamed geometry to come from a tiled scene manifest.

+

OOC Lifecycle

+

1. Tile parse

+

loadTile(entityId:) resolves the tile URL, then calls:

+
setEntityMeshAsync(
+    entityId: meshEntityId,
+    filename: ...,
+    withExtension: ...,
+    streamingPolicy: .auto,
+    blockRenderLoop: false
+)
+
+

The asset admission/classification stage decides whether this tile becomes:

+
    +
  • a full-load tile: geometry is uploaded immediately
  • +
  • an OOC tile: child stubs are registered and uploaded later
  • +
+

2. Stub registration

+

For an OOC tile, registerProgressiveStubEntity(...) creates one ECS stub per mesh leaf:

+
    +
  • transform and bounds are registered immediately
  • +
  • StreamingComponent starts as .unloaded
  • +
  • the stub is inserted into the octree
  • +
  • no GPU buffers are created yet
  • +
+

3. CPU registry

+

Each stub stores a CPUMeshEntry in ProgressiveAssetLoader, including:

+
    +
  • source MDLObject
  • +
  • vertex descriptor
  • +
  • unique mesh name
  • +
  • estimated GPU bytes
  • +
  • original loading policy
  • +
+

This is the warm CPU copy used for re-upload.

+

4. Incremental GPU upload

+

On streaming ticks, GeometryStreamingSystem evaluates tile-owned OCC stubs:

+
    +
  • near-band stubs are serialized with nearBandMaxConcurrentLoads
  • +
  • total mesh uploads are capped by maxConcurrentLoads
  • +
  • geometry budget checks can trigger texture shedding and eviction before new uploads
  • +
+

Successful upload transitions the stub from .loading to .loaded, increments the parent tile's OCC-ready counters, and emits normal residency events for batching.

+

5. Eviction

+

When the camera moves away or geometry pressure rises:

+
    +
  • unloadMesh(entityId:) clears RenderComponent.mesh
  • +
  • MemoryBudgetManager unregisters the GPU allocation
  • +
  • the CPU entry remains warm unless the root asset is explicitly cooled
  • +
+

This is why a normal OOC re-approach can re-upload without a disk read.

+

6. Warm-to-cold transition

+

On critical pressure, the engine may release warm CPU state:

+
ProgressiveAssetLoader.shared.releaseWarmAsset(rootEntityId:)
+
+

This frees the retained MDLAsset tree and CPU mesh buffers but preserves the rehydration context. A later re-approach reparses the root asset once and rebuilds the CPU registry.

+

Interaction with Tile Streaming

+

OOC is just one representation inside the broader tile system:

+
    +
  • full tile for near field
  • +
  • per-tile LOD for mid range
  • +
  • HLOD for far range
  • +
  • OOC child stubs for large tiles whose full geometry should not upload all at once
  • +
+

That is why the runtime documentation should describe OOC as an implementation detail of tile streaming rather than a separate public scene-loading mode.

+

Key Takeaways

+
    +
  • OOC still exists, but it is now subordinate to tile ownership.
  • +
  • ProgressiveAssetLoader is the CPU residency layer.
  • +
  • GeometryStreamingSystem is the GPU residency scheduler.
  • +
  • setEntityStreamScene(...) is the supported public streaming API surface.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/Architecture/progressiveAssetLoader/index.html b/site/Architecture/progressiveAssetLoader/index.html new file mode 100644 index 00000000..b719bba0 --- /dev/null +++ b/site/Architecture/progressiveAssetLoader/index.html @@ -0,0 +1,2450 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Progressive Asset Loader - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Progressive Asset Loader

+

TL;DR

+

ProgressiveAssetLoader is a CPU registry — its sole responsibility is storing CPUMeshEntry records for out-of-core stub entities and serving them to GeometryStreamingSystem on demand.

+
+

Note: This document describes the current architecture. The earlier tick-based progressive loader (per-frame job queue, PendingObjectItem, enqueue(job), tick() processing N meshes per frame) was replaced by the out-of-core stub system. tick() is retained as a no-op for call-site compatibility only.

+
+
+

What It Stores

+

When setEntityMeshAsync routes an asset through the out-of-core path it stores CPU-side geometry in one of two registries depending on whether the asset contains LOD groups:

+
    +
  • cpuMeshRegistry ([EntityID: CPUMeshEntry]) — one entry per stub entity (regular OOC assets)
  • +
  • cpuLODRegistry ([EntityID: [Int: CPUMeshEntry]]) — one entry per LOD level per LOD group entity (LOD+OOC assets)
  • +
+

Both registries store CPUMeshEntry records:

+
struct CPUMeshEntry {
+    let object: MDLObject           // MDLMesh with CPU-heap vertex/index data
+    let vertexDescriptor: MDLVertexDescriptor
+    let textureLoader: TextureLoader
+    let device: MTLDevice
+    let url: URL
+    let filename: String
+    let withExtension: String
+    let uniqueAssetName: String     // "Hull_A#42" — stable across load cycles
+    let estimatedGPUBytes: Int      // vertex + index bytes; used for pre-emptive budget reservation
+}
+
+

Entries are keyed by child entity ID. GeometryStreamingSystem retrieves them via retrieveCPUMesh(for:) when an entity enters streaming range, copies the MDL buffers into Metal-backed buffers, and registers a RenderComponent. The CPU entry is never removed on unload — re-approaching an evicted entity re-uploads from RAM with no disk I/O.

+

LOD CPU Registry (LOD+OOC Path)

+

When a USDZ asset contains LOD groups (top-level objects named Tree_LOD0, Tree_LOD1, etc.) and qualifies for OOC streaming, setEntityMeshAsync takes the LOD+OOC path instead of the per-stub path:

+
    +
  • One entity is created per LOD group (instead of one entity per MDLObject)
  • +
  • Each entity gets a LODComponent with stub LODLevels — empty mesh arrays, residencyState: .notResident
  • +
  • One CPUMeshEntry is stored in cpuLODRegistry[groupEntityId][lodIndex] for each LOD level
  • +
+
// LOD+OOC: per-level CPU entries, keyed by (group entity ID, LOD index)
+cpuLODRegistry[treeEntityId] = [
+    0: CPUMeshEntry(object: tree_LOD0_MDLObject, uniqueAssetName: "Tree_LOD0", ...),
+    1: CPUMeshEntry(object: tree_LOD1_MDLObject, uniqueAssetName: "Tree_LOD1", ...),
+    2: CPUMeshEntry(object: tree_LOD2_MDLObject, uniqueAssetName: "Tree_LOD2", ...),
+]
+
+

GeometryStreamingSystem detects the LOD+OOC path via hasCPULODData(for:) and calls uploadActiveLODFromCPU instead of uploadFromCPUEntry. This uploads all LOD levels from the CPU registry in one pass, then sets the render component to the level appropriate for the current camera distance. Subsequent LOD switches are handled by LODSystem which swaps renderComponent.mesh from the already-resident lodComponent.lodLevels array — no additional streaming needed until the entity is evicted and re-enters range.

+
+

The MDLAsset Lifetime Problem

+

MDLMeshBufferDataAllocator (used by parseAssetAsync) backs all CPU buffers via the MDLAsset container. If the asset is released, all child MDLMesh CPU pointers become dangling.

+

ProgressiveAssetLoader solves this with rootAssetRefs:

+
private var rootAssetRefs: [EntityID: MDLAsset] = [:]
+
+

storeAsset(_:for:) pins the MDLAsset to the root entity ID. It stays alive until removeOutOfCoreAsset(rootEntityId:) is called at entity destruction time.

+
+

Background Texture Prewarm

+

storeAsset(_:for:) immediately fires a background Task at .userInitiated priority to call loadTextures() before any mesh enters streaming range:

+
func storeAsset(_ asset: MDLAsset, for rootEntityId: EntityID) {
+    // Pin the asset and create the per-asset texture lock.
+    lock.lock()
+    rootAssetRefs[rootEntityId] = asset
+    assetTextureLocks[rootEntityId] = NSLock()
+    lock.unlock()
+    // Kick off background prewarm immediately.
+    prewarmTexturesAsync(for: rootEntityId)
+}
+
+

The prewarm task acquires the per-asset texture lock, calls ensureTexturesLoaded, and releases the lock — all off the critical path. By the time the first mesh enters streaming range, loadTextures() has typically already completed, so the first-upload path sees a no-op ensureTexturesLoaded call and zero lock wait.

+

activePrewarmRoots tracks which roots have an in-flight prewarm task. GeometryStreamingSystem queries isPrewarmActive(for:) in the dispatch loop and defers uploading entities for that root until the prewarm completes. This prevents the first batch of uploads from blocking on the texture lock for the full remaining prewarm duration.

+
+

Per-Asset Texture Serialization

+

MDLAsset is not thread-safe. Two GeometryStreamingSystem tasks uploading different meshes from the same asset concurrently can race during loadTextures(). ProgressiveAssetLoader prevents this with a per-asset NSLock:

+
private var assetTextureLocks: [EntityID: NSLock] = [:]
+
+

storeAsset creates the lock alongside the asset reference. Every upload task brackets only ensureTexturesLoaded with the lock — the lock is released before makeMeshesFromCPUBuffers:

+
ProgressiveAssetLoader.shared.acquireAssetTextureLock(for: rootId)
+ProgressiveAssetLoader.shared.ensureTexturesLoaded(for: rootId)
+ProgressiveAssetLoader.shared.releaseAssetTextureLock(for: rootId)
+// makeMeshesFromCPUBuffers runs without the lock — MDLAsset is read-only after loadTextures()
+
+

After loadTextures() completes the MDLAsset is in a stable read-only state. Concurrent makeMeshesFromCPUBuffers calls from the same asset are safe without the lock, so all three upload slots can proceed in parallel once the prewarm is done.

+

Only the ensureTexturesLoaded call is serialized per asset. Meshes from different assets upload concurrently without any contention.

+
+

Deferred loadTextures()

+

Large assets skip asset.loadTextures() at parse time to avoid the OOM risk of decompressing all textures before the app is interactive. The call is deferred via ensureTexturesLoaded:

+
func ensureTexturesLoaded(for rootEntityId: EntityID) {
+    // Must be called while per-asset texture lock is held.
+    // Calls asset.loadTextures() exactly once per asset lifetime.
+}
+
+

assetTexturesLoaded: Set<EntityID> ensures the call happens exactly once. In normal operation the prewarm task wins the race and marks the asset loaded before any upload task reaches ensureTexturesLoaded, making the upload-path call a no-op.

+
+

API Surface

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodPurpose
storeCPUMesh(_:for:)Store a CPUMeshEntry keyed by child entity ID (regular OOC)
retrieveCPUMesh(for:)Fetch the entry for GeometryStreamingSystem upload (regular OOC)
removeCPUMesh(for:)Remove a single entry (rarely needed; prefer removeOutOfCoreAsset)
storeCPULODMesh(_:for:lodIndex:)Store a CPUMeshEntry for one LOD level of a LOD group entity
retrieveCPULODMesh(for:lodIndex:)Fetch the entry for a specific LOD level
retrieveAllCPULODMeshes(for:)Fetch all LOD-level entries for a group entity
hasCPULODData(for:)Returns true if the entity was registered via the LOD+OOC path
removeCPULODEntry(for:)Remove all LOD entries for a group entity
storeAsset(_:for:)Pin an MDLAsset, create its per-asset texture lock, and kick off background prewarm
isPrewarmActive(for:)Returns true while the background prewarm task holds the texture lock for this root
registerChildren(_:for:)Associate child entity IDs with a root for bulk cleanup
acquireAssetTextureLock(for:)Lock before calling ensureTexturesLoaded
releaseAssetTextureLock(for:)Unlock immediately after ensureTexturesLoaded — before GPU upload work
ensureTexturesLoaded(for:)Call loadTextures() exactly once per asset (must hold texture lock)
removeOutOfCoreAsset(rootEntityId:)Release all CPU entries (both registries) + MDLAsset for a destroyed root entity
cancelAll()Release everything — use on scene reset or test teardown
tick()No-op stub; retained for call-site compatibility
+
+

Data Flow

+
setEntityMeshAsync (out-of-core path — regular OOC)
+  │
+  ├─ parseAssetAsync()               → MDLAsset in CPU RAM (no GPU spike)
+  ├─ registerProgressiveStubEntity() → N ECS stubs, StreamingComponent(.unloaded)
+  ├─ storeCPUMesh(entry, for: childId) × N  → cpuMeshRegistry
+  ├─ storeAsset(asset, for: rootId)   → rootAssetRefs, assetTextureLocks
+  │     └─ prewarmTexturesAsync()    → background Task: acquireLock / loadTextures() / releaseLock
+  ├─ registerChildren(childIds, for: rootId)
+  └─ completion(true)                → GeometryStreamingSystem picks up stubs automatically (always running)
+
+setEntityMeshAsync (out-of-core path — LOD+OOC)
+  │
+  ├─ parseAssetAsync()               → MDLAsset in CPU RAM
+  ├─ detectImportedLODGroups()        → N LOD groups detected
+  ├─ (per group) createEntity + LODComponent(stubs) + StreamingComponent(.unloaded)
+  ├─ storeCPULODMesh(entry, for: groupId, lodIndex:) × (N groups × L levels)  → cpuLODRegistry
+  ├─ storeAsset(asset, for: rootId)
+  │     └─ prewarmTexturesAsync()    → background Task: acquireLock / loadTextures() / releaseLock
+  ├─ registerChildren(groupEntityIds, for: rootId)
+  └─ completion(true)
+
+GeometryStreamingSystem (adaptive tick: 16 ms during backlog, 100 ms steady-state)
+  │
+  ├─ isPrewarmActive(rootId)?  → YES → defer all entities for this root (slots stay free)
+  │
+  ├─ entity within streamingRadius && state == .unloaded
+  │   ├─ hasCPULODData?  → YES → uploadActiveLODFromCPU()
+  │   │     ├─ retrieveAllCPULODMeshes(for: entityId)
+  │   │     ├─ acquireAssetTextureLock / ensureTexturesLoaded / releaseAssetTextureLock
+  │   │     ├─ makeMeshesFromCPUBuffers() × L levels  ← lock released; parallel uploads safe
+  │   │     ├─ LODComponent.lodLevels[i].residencyState = .resident  for each uploaded level
+  │   │     └─ registerRenderComponent() at distance-appropriate LOD
+  │   │
+  │   └─ hasCPULODData?  → NO  → retrieveCPUMesh / uploadFromCPUEntry (regular OOC)
+  │         ├─ acquireAssetTextureLock / ensureTexturesLoaded / releaseAssetTextureLock
+  │         ├─ makeMeshesFromCPUBuffers()  ← lock released; parallel uploads safe
+  │         └─ registerRenderComponent()
+  │
+  └─ entity beyond unloadRadius && state == .loaded
+      └─ render.mesh = []  (cpu entries kept — re-upload from RAM on re-approach)
+
+destroyAllEntities / scene reset
+  └─ removeOutOfCoreAsset(rootEntityId:)  → frees both CPU registries + MDLAsset
+
+
+

Memory Model at Steady State

+
CPU RAM:  all leaf meshes' MDLMesh vertex/index data — always resident
+GPU RAM:  only entities within streamingRadius — uploaded on demand
+Disk:     read exactly once at parse time
+
+

This trades a modest CPU-RAM footprint for predictable GPU memory usage and zero-latency re-uploads after eviction.

+
+

Cleanup

+

Call removeOutOfCoreAsset(rootEntityId:) when destroying a root entity to free its CPU-heap geometry and texture-lock state:

+
ProgressiveAssetLoader.shared.removeOutOfCoreAsset(rootEntityId: rootId)
+
+

destroyAllEntities does not call this automatically — you must call it explicitly if you are managing entity lifetimes outside the engine's destruction path.

+

For full teardown (scene resets, tests):

+
ProgressiveAssetLoader.shared.cancelAll()
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/Architecture/renderingSystem/index.html b/site/Architecture/renderingSystem/index.html new file mode 100644 index 00000000..af4b7451 --- /dev/null +++ b/site/Architecture/renderingSystem/index.html @@ -0,0 +1,2722 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Rendering System - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

RenderingSystem — How It Works

+

The rendering system's job is to take the current set of visible entities and turn them into pixels on screen every frame. It does this in two distinct phases: pre-render compute (GPU culling and sorting) followed by a render graph — a dependency-ordered DAG of passes that each write into shared textures until the final image lands on the drawable.

+

The entry point is UpdateRenderingSystem(in view: MTKView), called once per frame from the MTKView draw loop.

+
+

Step 0: The Visible Entity List

+

Before any rendering begins, the system needs to know which entities are visible. This is managed through a triple-buffer called tripleVisibleEntities:

+
visibleEntityIds = tripleVisibleEntities.snapshotForRead(frame: cullFrameIndex)
+
+

The key insight here is that visibleEntityIds is not rebuilt from scratch each frame. It is the result of the previous frame's GPU frustum cull — a compute pass that ran last frame and wrote its output into the triple-buffer. The current frame reads that result and uses it immediately.

+

Why triple-buffered? The GPU may still be consuming last frame's cull output while the CPU is already preparing the next frame. Three slots prevent read/write races across overlapping frames.

+

While loading: When AssetLoadingGate.shared.isLoadingAny is true, the snapshot step is skipped entirely. The last-known-good visibleEntityIds is reused. This prevents reading from ECS storage while asset loading is mutating it on a background thread.

+
+

Step 1: Command Buffer Slot Acquisition

+
commandBufferSemaphore.wait()
+renderInfo.currentInFlightFrameSlot = acquireUniformFrameSlot()
+
+

The engine allows at most 3 command buffers in flight at once (matching the triple-buffer count). The semaphore blocks the CPU if the GPU is still consuming all three slots.

+

acquireUniformFrameSlot() returns the index into the per-frame uniform buffer ring. Because the CPU writes entity transforms and camera matrices into these buffers while the GPU reads them, each in-flight frame needs its own slot to avoid corruption.

+
+

Step 2: Root Transform Propagation

+
SceneRootTransform.shared.updateIfNeeded()
+
+

Before any uniforms are uploaded, dirty transforms are propagated down the scene graph. An entity whose parent moved needs its WorldTransformComponent updated before the model matrix is sent to the GPU. This runs lazily — only if something was marked dirty since the last frame.

+
+

Step 3: Pre-Render Compute Passes

+

These three compute dispatches run before any render encoder is opened. They prepare data that the render passes will consume.

+

3a. Frustum Culling → performFrustumCulling(commandBuffer:)

+

A compute shader tests every entity's axis-aligned bounding box (EntityAABB) against the camera's 6 frustum planes. Entities outside the frustum are excluded.

+

The result is written into tripleVisibleEntitiesfor the next frame. So culling is always one frame behind rendering. This is an intentional latency trade-off: GPU-driven culling is far faster than CPU culling, and one frame of lag is imperceptible.

+

In addition to writing the GPU visibility result, executeFrustumCulling stores the current-frame frustum in the module-level variable currentFrameFrustum. This frustum is the padded, CPU-side version built from the view-projection matrix. It is read later in the same frame by the batched render passes for cluster-level AABB culling of BatchGroups (see G-Buffer Passes and Shadow Passes).

+

For XR, a reduce-scan variant runs the test against both eyes simultaneously.

+

3b. Gaussian Depth → executeGaussianDepth(commandBuffer)

+

For entities carrying a GaussianComponent (3D Gaussian splat data), a compute pass calculates the camera-space depth of each splat. This depth value is used as the sort key in the next step.

+

3c. Bitonic Sort → executeBitonicSort(commandBuffer)

+

A GPU bitonic sort reorders the Gaussian splats back-to-front by depth. Gaussian splats must be composited in this order for correct alpha blending. The sort runs entirely on the GPU and its output feeds directly into the Gaussian render pass later in the graph.

+
+

Step 4: Building the Render Graph → buildGameModeGraph()

+

Rather than hard-coding a linear sequence of passes, the engine constructs a directed acyclic graph (DAG) of RenderPass nodes each frame:

+
struct RenderPass {
+    let id: String
+    var dependencies: [String]
+    var execute: (MTLCommandBuffer) -> Void
+}
+
+

Each pass declares which other passes must complete before it can run. buildGameModeGraph() assembles these nodes into a dictionary and returns it. Nothing executes yet — this is purely declarative.

+

The full graph for a typical frame looks like this:

+
environment/grid
+    └── shadow
+            └── batchedShadow
+                    └── model ──────────────────────────── gaussian
+                            └── batchedModel                    │
+                                    ├── ssao                    │
+                                    └── lightPass               │
+                                            └── transparency    │
+                                                    └── spatialDebug
+                                                            └── [post-processing chain]
+                                                                        └── precomp ◄── (gaussian joins here)
+                                                                                └── look
+                                                                                        └── outputTransform
+
+

Base Pass (environment or grid)

+

The graph always starts with a background pass whose type depends on the platform and rendering mode:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ContextPassPurpose
macOS/iOS with HDR skyenvironmentRenders the IBL skybox cubemap
macOS/iOS without HDRgridRenders the editor debug grid
XR passthrough (mixed)(none)Camera feed is the background
XR full immersionenvironmentSkybox inside the headset
+

This pass has no dependencies — it is always the root of the graph.

+

Shadow Passes

+
shadow → batchedShadow
+
+

Both passes render scene geometry from the directional light's point of view into a shadow map depth texture. No color is written — only depth. The renderer checks entityToBatch and routes each entity to the appropriate pass: +- Regular entities → shadowExecution +- Batched entities → batchedShadowExecution

+

batchedShadowExecution uses cluster-level frustum culling: it calls visibleBatchGroupsSnapshot() which tests each BatchGroup's precomputed world-space AABB against currentFrameFrustum. Only groups whose AABB intersects the frustum are submitted. This replaces the previous entity→batchId derivation and operates at batch-group granularity — one AABB test per group instead of one per entity.

+

The shadow map produced here is consumed later by lightPass.

+

G-Buffer Passes (deferred rendering)

+
model → batchedModel → ssao → lightPass
+
+

This is the core of the deferred rendering pipeline. Entities do not produce a shaded color here — they write raw surface data into multiple render targets (the G-Buffer):

+
    +
  • Albedo — base color
  • +
  • Normal — world-space surface normal
  • +
  • World position — reconstructed from depth
  • +
  • Material — roughness, metalness, emissive flags
  • +
+

modelExecution iterates visibleEntityIds. For each entity that is not batched: +- Binds vertex/index buffers +- Uploads the model matrix, normal matrix, and camera uniforms into the current in-flight frame slot +- Issues a draw call per mesh submesh

+

batchedModelExecution uses cluster-level frustum culling: it calls visibleBatchGroupsSnapshot() which tests each BatchGroup's precomputed world-space AABB against currentFrameFrustum using isAABBInFrustum. The result — groups whose AABB intersects the frustum — is cached for the frame and shared with batchedShadowExecution. Each surviving group is then submitted as a single draw call with its merged vertex and index buffers.

+

ssaoOptimizedExecution reads the G-Buffer normals and depth and produces a screen-space ambient occlusion texture. Blurring is handled internally — no separate blur nodes appear in the graph.

+

lightExecution is where the entity first appears fully lit. It reads all four G-Buffer textures plus the shadow map and SSAO texture and combines them into a single HDR scene color texture using the deferred lighting algorithm. This is a full-screen quad pass — geometry is never touched again after the G-Buffer step.

+
+

Why deferred? Deferred rendering means the lighting cost scales with the number of lit pixels, not the number of geometry draw calls × number of lights. Complex scenes with many overlapping objects benefit greatly because each pixel is only shaded once, regardless of how many triangles projected onto it.

+
+

Transparency Pass

+
RenderPass(id: "transparency", dependencies: ["lightPass"])
+
+

Transparent materials cannot go through the G-Buffer — they require alpha blending which deferred rendering cannot express per-fragment. These entities are rendered forward in a separate pass on top of the deferred lit scene color. They depend on lightPass being complete so they composite correctly against the opaque scene.

+

Spatial Debug Pass

+
RenderPass(id: "spatialDebug", dependencies: ["transparency"])
+
+

Draws wireframe AABB overlays for debug purposes. Runs last in the geometry chain so it draws on top of everything.

+

Gaussian Pass

+
RenderPass(id: "gaussian", dependencies: ["model"])
+
+

Renders the back-to-front-sorted Gaussian splats using the indices produced by the bitonic sort. This pass depends on "model" because it needs the depth buffer that was populated during the G-Buffer model pass — splats use that depth to correctly composite against solid geometry.

+

Note that Gaussian does not depend on lightPass, transparency, or the post-processing chain. It runs in parallel with those in the dependency graph and merges back at precomp.

+

Post-Processing Chain

+
spatialDebug → depthOfField → chromatic → bloomThreshold
+    → blur_hor_1 → blur_ver_1 → blur_hor_2 → blur_ver_2
+    → bloomComposite → vignette
+
+

postProcessingEffects() builds this chain dynamically inside buildGameModeGraph(). Each effect reads from the previous pass's output texture and writes to its own.

+

Fast path: If every effect (BloomThresholdParams, VignetteParams, ChromaticAberrationParams, DepthOfFieldParams) is disabled, the entire chain is replaced by a single bypass pass that points the post-process descriptor at the deferred output texture directly. This avoids allocating ~142 MB of intermediate render targets that would be unused.

+

The number of blur iterations is driven by BloomThresholdParams.shared.enabled — when bloom is on, two horizontal/vertical pairs are dispatched; when off, zero. The loop that generates blur nodes in the graph is:

+
for i in 0 ..< blurPassCount {
+    // horizontal blur pass
+    // vertical blur pass
+}
+
+

So the graph topology literally changes based on whether bloom is enabled.

+

Pre-Composite Pass

+
RenderPass(id: "precomp", dependencies: [postProcessID, gaussianPass.id])
+
+

This is the convergence point of the two parallel tracks. The post-processed scene color and the Gaussian splat render both arrive here and are composited into a single texture. Neither track can be finalized without the other.

+

Look Pass (Color Grading)

+
RenderPass(id: "look", dependencies: ["precomp"])
+
+

Applies lift/gamma/gain color correction and optional LUT-based grading to the composited image.

+

Output Transform Pass

+
RenderPass(id: "outputTransform", dependencies: ["look"])
+
+

Tone maps the HDR scene color into the display's color space (SDR or EDR depending on the target). This is the terminal node of the graph — its output texture is what gets presented to the drawable.

+
+

Step 5: Graph Execution

+

With the graph assembled, the engine sorts and executes it:

+
let sortedPasses = try! topologicalSortGraph(graph: graph)
+executeGraph(graph, sortedPasses, commandBuffer)
+
+

topologicalSortGraph performs a depth-first search over the dependency edges and returns a [String] of pass IDs in a valid execution order — every pass appears after all its dependencies.

+

executeGraph iterates that list and calls each pass's execute closure, encoding Metal render or compute commands into the shared commandBuffer. All passes share one command buffer, so Metal can pipeline them efficiently on the GPU.

+
+

Step 6: HZB Depth Pyramid

+
buildHZBDepthPyramid(commandBuffer)
+
+

After the render graph finishes, the depth texture produced during the G-Buffer model pass is downsampled into a hierarchical Z-buffer mip pyramid. This feeds next frame's occlusion culling — a coarse depth mip level can quickly reject large occluded objects before the fine cull.

+

This is intentionally scheduled here, after the render graph and before commit(), so the HZB is built from the freshest depth available and ready for the next frame's culling compute dispatch.

+
+

Step 7: Present and Commit

+
commandBuffer.present(drawable)
+commandBuffer.commit()
+
+

The completion handler fires on the GPU thread when the command buffer finishes executing:

+
    +
  • commandBufferSemaphore.signal() — frees one slot, allowing the CPU to encode the next frame
  • +
  • needsFinalizeDestroys = true — deferred ECS entity removal can proceed safely now that the GPU is done with this frame's data
  • +
  • MemoryBudgetManager.shared.markUsed(entityIds:) — records which entities were rendered so the memory budget manager knows what to keep resident and what to evict
  • +
+
+

The Full Frame in One Picture

+
[CPU] snapshotVisibleEntities (from last frame's cull)
+[CPU] wait on semaphore / acquire uniform slot
+[CPU] propagate dirty transforms
+        │
+        ▼
+[GPU compute] frustumCulling   → writes next frame's visibleEntityIds
+[GPU compute] gaussianDepth    → depth per splat
+[GPU compute] bitonicSort      → sort splats back-to-front
+        │
+        ▼
+[CPU] buildGameModeGraph()     → construct render pass DAG
+[CPU] topologicalSortGraph()   → linearize pass order
+        │
+        ▼
+[GPU render] environment/grid
+[GPU render] shadow + batchedShadow   (depth from light POV)
+[GPU render] model + batchedModel     (entity → G-Buffer)
+[GPU render] ssao                     (occlusion from G-Buffer)
+[GPU render] lightPass                (entity appears fully lit)
+[GPU render] transparency             (forward-rendered alphas)
+[GPU render] spatialDebug             (debug overlays)
+[GPU render] gaussian                 (sorted splats)
+[GPU render] post-processing chain    (DOF, bloom, vignette)
+[GPU render] precomp                  (merge scene + splats)
+[GPU render] look                     (color grading)
+[GPU render] outputTransform          (tone map → drawable)
+[GPU compute] buildHZB                (depth pyramid for next frame)
+        │
+        ▼
+[CPU] present drawable + commit
+[GPU→CPU callback] signal semaphore, mark memory used
+
+
+

Why a Render Graph Instead of a Fixed Pass Order?

+

A fixed sequence of if statements works fine until the graph needs to change — when post-processing is disabled, when XR changes the base pass, or when the number of bloom blur iterations varies based on settings. A render graph makes these variations declarative: each pass states what it needs, and the topology sorts itself. Adding a new pass means adding one RenderPass node with its dependency list — the rest of the system adapts automatically.

+

It also makes the dependency structure explicit and auditable. If a pass reads a texture produced by another pass, that relationship is encoded as a graph edge rather than buried in execution order assumptions.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/Architecture/streamingCacheLifecycle/index.html b/site/Architecture/streamingCacheLifecycle/index.html new file mode 100644 index 00000000..3085c758 --- /dev/null +++ b/site/Architecture/streamingCacheLifecycle/index.html @@ -0,0 +1,2377 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Streaming Cache Lifecycle - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Streaming Cache Lifecycle

+

This document describes how the current streaming architecture manages geometry across three layers:

+ + + + + + + + + + + + + + + + + + + + + +
LayerOwns
GeometryStreamingSystemDistance-based residency decisions
ProgressiveAssetLoaderWarm/cold CPU mesh state for tile-owned OOC assets
MeshResourceManagerShared GPU mesh cache for non-OOC and disk-backed reload paths
+

Two Residency Modes

+

Full-load / cache-backed meshes

+

For eager loads, MeshResourceManager owns the shared mesh data:

+
    +
  • loadMesh(url:meshName:) returns cached or freshly loaded meshes
  • +
  • retain(...) increments residency ownership for an entity
  • +
  • release(entityId:) decrements the reference count
  • +
  • evictUnused() frees GPU data when the ref count reaches zero
  • +
+

Tile-owned OOC meshes

+

For large streamed tiles, ProgressiveAssetLoader owns the CPU source data:

+
    +
  • CPUMeshEntry stores the parsed CPU mesh buffers
  • +
  • GeometryStreamingSystem uploads those buffers on demand
  • +
  • eviction normally drops only GPU residency
  • +
  • critical-pressure cooling may release the CPU copy too
  • +
+

Current Lifecycle

+

1. Tiled scene registration

+

setEntityStreamScene(...) registers lightweight TileComponent stubs only, parented under the supplied root entity. No geometry is resident yet.

+

2. Tile parse

+

When a tile enters prefetch range, loadTile(entityId:) parses the tile payload.

+

At that point the runtime chooses one of two outcomes:

+
    +
  • full-load tile: render entities are GPU-resident immediately
  • +
  • OOC tile: child StreamingComponent stubs are registered and backed by CPUMeshEntry
  • +
+

3. OOC upload

+

For OOC stubs, GeometryStreamingSystem.loadMesh(...):

+
    +
  1. checks tile ownership
  2. +
  3. reserves an active streaming slot
  4. +
  5. uploads from ProgressiveAssetLoader when warm
  6. +
  7. falls back to cold rehydration if the root was cooled
  8. +
  9. marks the entity loaded and emits residency change events
  10. +
+

4. Full-load cache use

+

For eager/disk-backed meshes, the load path uses MeshResourceManager:

+
    +
  1. loadMesh(...)
  2. +
  3. retain(...)
  4. +
  5. entity-local copyWithNewUniformBuffers()
  6. +
  7. apply to RenderComponent
  8. +
+

The copied uniform buffers are per-entity, but the underlying cached geometry is shared.

+

5. Eviction

+

When geometry leaves range or memory pressure rises:

+
    +
  • unloadMesh(...) clears the entity's live mesh reference
  • +
  • MeshResourceManager.release(entityId:) decrements shared cache refs when applicable
  • +
  • OOC stubs keep their CPU source warm unless explicitly cooled
  • +
+

6. Cache cleanup

+

MeshResourceManager.evictUnused() removes zero-ref cached meshes. This is the first stage of geometry relief before more aggressive runtime eviction.

+

7. CPU cooling

+

Under critical memory pressure the engine may call:

+
ProgressiveAssetLoader.shared.releaseWarmAsset(rootEntityId:)
+
+

That frees the retained MDLAsset tree and child CPU buffers for that streamed root while preserving enough context to reparse later.

+

Why the Split Exists

+

The split cache model supports both fast reuse and bounded memory:

+
    +
  • MeshResourceManager is efficient for shared eager meshes and disk-backed reloads
  • +
  • ProgressiveAssetLoader avoids reparsing large tiles on every near/far traversal
  • +
  • GeometryStreamingSystem arbitrates both with the same distance, frustum, and budget logic
  • +
+

Practical Reading

+

When reviewing a streamed tile today, think of residency in this order:

+
    +
  1. Is the tile stub in range?
  2. +
  3. Did the tile classify as full-load or OOC?
  4. +
  5. If OOC, is the CPU source warm or cold?
  6. +
  7. Is the GPU copy currently resident?
  8. +
  9. Is batching representing the entity directly or via a cell artifact?
  10. +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/Architecture/streamingRegionManager/index.html b/site/Architecture/streamingRegionManager/index.html new file mode 100644 index 00000000..207e204a --- /dev/null +++ b/site/Architecture/streamingRegionManager/index.html @@ -0,0 +1,2539 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Streaming Region Manager - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

StreamingRegionManager

+

StreamingRegionManager provides a lightweight, region-based geometry streaming API that is an alternative to the tile manifest system (setEntityStreamScene). Rather than consuming a JSON manifest, callers register explicit StreamingRegion values — each describing a world-space AABB and a list of asset references — and the manager loads or unloads them based on camera proximity.

+

Use this API for handcrafted or procedurally defined streaming zones where a manifest file is not practical. Use setEntityStreamScene for large outdoor/indoor scenes exported by the Blender pipeline.

+
+

When to use which API

+ + + + + + + + + + + + + + + + + + + + + +
ScenarioPreferred API
Large scene exported by the Blender pipeline (tiles, HLOD, LOD levels)setEntityStreamScene(entityId:url:)
Handcrafted streaming zones (e.g. dungeon rooms, level sectors)StreamingRegionManager
Always-resident objects (characters, props, HUD elements)setEntityMeshAsync(entityId:filename:withExtension:)
+
+

Core Types

+

StreamingRegion

+
public struct StreamingRegion: Identifiable {
+    public let id: UUID
+    public let bounds: AABB          // World-space AABB that triggers load/unload
+    public let priority: Int         // Higher = loaded first when slots compete
+    public let assets: [AssetReference]
+    public var state: StreamingState
+    public var loadedEntities: [EntityID]
+    public var estimatedMemorySize: Int  // Bytes; used for memory budget gate
+}
+
+

AssetReference names a single asset by filename and extension:

+
public struct AssetReference: Equatable, Hashable {
+    public let filename: String
+    public let fileExtension: String
+}
+
+

StreamingState

+
unloaded → loading → loaded → unloading → unloaded
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
StateMeaning
.unloadedNo geometry loaded for this region
.loadingAsync load task running
.loadedAll region assets are GPU-resident
.unloadingTeardown in progress
+
+

Configuration

+
StreamingRegionManager.shared.enabled = true
+StreamingRegionManager.shared.streamingRadius = 100.0   // load within this distance
+StreamingRegionManager.shared.unloadRadius = 150.0      // unload beyond this distance
+StreamingRegionManager.shared.maxConcurrentLoads = 3    // max simultaneous loads
+StreamingRegionManager.shared.checkInterval = 0.5       // seconds between evaluations
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyDefaultNotes
streamingRadius100 mCamera-to-AABB distance inside which the region loads
unloadRadius150 mCamera-to-AABB distance beyond which the region unloads
maxConcurrentLoads3Hard cap on simultaneous region load tasks
checkInterval0.5 sTick rate; skips frames between evaluations
+

These are shared global defaults. Per-region load distance is not currently configurable; all regions use the same radii. For per-region control, use setEntityStreamScene with a manifest.

+
+

Usage

+

Registering regions

+
let region = StreamingRegion(
+    bounds: AABB(min: simd_float3(-50, 0, -50), max: simd_float3(50, 10, 50)),
+    priority: 1,
+    assets: [
+        AssetReference(filename: "dungeon_room_A", withExtension: "usdz"),
+        AssetReference(filename: "dungeon_room_A_props", withExtension: "usdz"),
+    ],
+    estimatedMemorySize: 40_000_000  // 40 MB estimate
+)
+
+StreamingRegionManager.shared.registerRegion(region)
+
+

Updating each frame

+
// Call from your game loop:
+StreamingRegionManager.shared.update(cameraPosition: cameraPos, deltaTime: dt)
+
+

update() throttles internally — real work only runs every checkInterval seconds.

+

Removing regions

+
StreamingRegionManager.shared.unregisterRegion(id: region.id)
+
+

Unregistering cancels any in-flight load task immediately.

+

Force load / unload (testing or cutscenes)

+
let didLoad = await StreamingRegionManager.shared.forceLoadRegion(id: region.id)
+let didUnload = await StreamingRegionManager.shared.forceUnloadRegion(id: region.id)
+
+
+

Per-frame Update Logic

+

Each tick (every checkInterval seconds):

+
    +
  1. Find load candidates.unloaded regions whose AABB is within streamingRadius of the camera. Sorted by priority (descending) then distance (ascending).
  2. +
  3. Find unload candidates.loaded regions whose AABB is beyond unloadRadius.
  4. +
  5. Unload first — frees memory before committing to new loads.
  6. +
  7. Load up to maxConcurrentLoads candidates — each spawns an async Task.
  8. +
+

Distance is measured as the closest point on the region AABB to the camera position (AABB distance, not center distance), so an AABB that surrounds the camera has distance 0.

+
+

Load Path

+

loadRegion(id:) (internal):

+
    +
  1. Marks region .loading.
  2. +
  3. Checks MemoryBudgetManager.canAccept(sizeBytes:). If the budget is full, attempts evictLRU before proceeding; if still full, marks region .unloaded and returns.
  4. +
  5. For each AssetReference in region.assets:
  6. +
  7. Calls createEntity() + setEntityMeshAsync(entityId:filename:withExtension:).
  8. +
  9. Registers memory with MemoryBudgetManager for the root entity and all children.
  10. +
  11. Marks region .loaded; records loadedEntities.
  12. +
  13. Emits AssetResidencyChangedEvent(isResident: true) for each entity (including children) so BatchingSystem and LODSystem see the new geometry.
  14. +
+
+

Unload Path

+

unloadRegion(id:) (internal):

+
    +
  1. Marks region .unloading.
  2. +
  3. Emits AssetResidencyChangedEvent(isResident: false) for all entities (children first) before destroying them — ensures BatchingSystem removes them from pending queues cleanly.
  4. +
  5. Calls MemoryBudgetManager.unregisterMesh(entityId:) for all entities.
  6. +
  7. Calls destroyEntity(entityId:) for each root (cascades to children).
  8. +
  9. Marks region .unloaded; clears loadedEntities.
  10. +
+
+

Stats

+
let stats = StreamingRegionManager.shared.getStats()
+// stats.totalRegions, loadedRegions, loadingRegions, activeLoads
+// stats.totalRootEntities, totalEntitiesWithChildren
+// stats.regionMemory (actual GPU bytes from MemoryBudgetManager)
+// stats.estimatedMemory (sum of user-supplied estimates)
+// stats.totalEngineMemory (entire engine, from MemoryBudgetManager)
+
+
+

Relationship to GeometryStreamingSystem

+

StreamingRegionManager is independent of GeometryStreamingSystem. They can run simultaneously — for example, a tile-streamed outdoor scene (setEntityStreamScene) with handcrafted interior sectors (StreamingRegionManager). Both systems share MemoryBudgetManager, so memory pressure from one is visible to the other.

+

StreamingRegionManager does not use the octree, frustum gating, prefetch radius, grace-period teardown, or HLOD/LOD systems. It is intentionally simpler. For any of those features, use setEntityStreamScene with a manifest.

+
+

See Also

+ + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/Architecture/textureStreamingSystem/index.html b/site/Architecture/textureStreamingSystem/index.html new file mode 100644 index 00000000..3e455c07 --- /dev/null +++ b/site/Architecture/textureStreamingSystem/index.html @@ -0,0 +1,3189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Texture Streaming System - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + + + + + + + +

Texture Streaming System

+

TextureStreamingSystem.swift dynamically adjusts the resolution of textures on entities based on their distance from the camera. Instead of keeping every texture at full resolution all the time, it streams textures up or down as the player moves through the scene — saving GPU memory while keeping nearby geometry crisp.

+
+

Scenario: A City Block with 500 Buildings

+

Imagine a USDZ scene with a city block containing 500 buildings. Each building has a RenderComponent with meshes and submeshes, and each submesh has a Material containing up to four PBR textures:

+
    +
  • Base Color (sRGB)
  • +
  • Roughness (linear)
  • +
  • Metallic (linear)
  • +
  • Normal (linear)
  • +
+

At full resolution, each building's textures might be 2048×2048 or larger. Loading all 500 buildings at full resolution at once would immediately exhaust GPU memory, causing frame drops or crashes.

+

The TextureStreamingSystem solves this by managing three quality tiers and promoting/demoting each building's textures as the camera moves.

+
+

Quality Tiers

+

The system operates with three tiers, controlled by two distance thresholds:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
TierConditionMax Dimension
Fulldistance <= upgradeRadius (default 12m)Native source resolution (nil cap)
MediumupgradeRadius < distance <= downgradeRadius (default 20m)maxTextureDimension (1024px on macOS, 768px on visionOS)
Minimumdistance > downgradeRadiusminimumTextureDimension (256px on macOS, 192px on visionOS)
+

On first import, TextureLoader caps all textures at minimumTextureDimension (256px). The streaming system then only upgrades as the camera approaches — it never immediately downgrades a freshly-loaded entity.

+
+

The Update Loop

+

Every frame, the game loop calls:

+
TextureStreamingSystem.shared.update(cameraPosition: ..., deltaTime: ...)
+
+

Throttle: The system only does real work every updateInterval seconds (default 0.2s). This prevents spending every frame scanning all entities.

+

Concurrency cap: At most maxConcurrentOps (default 3) async streaming operations run simultaneously. If all slots are busy, the tick exits early.

+
Frame N arrives
+ └─ timeSinceLastUpdate += deltaTime
+ └─ if < 0.2s → return (skip this frame)
+ └─ availableSlots = maxConcurrentOps − activeOps.count
+ └─ if 0 slots → return
+ └─ Priority-0 burst pass: drain priorityEntities (tile-freshly-loaded)
+ └─ Priority-1 pass: visible entities
+ └─ Priority-2 pass: upgraded-but-not-visible entities
+
+
+

Priority-0 Burst Pass: Freshly-Loaded Tile Entities

+

When a tile finishes loading, GeometryStreamingSystem calls notifyEntitiesReady(_:) to register the tile's render-descendant entity IDs as priority candidates:

+
TextureStreamingSystem.shared.notifyEntitiesReady(tileRenderIds)
+
+

On the next update tick, these entities are processed before the normal visible-entity pass. This ensures newly-streamed-in tile geometry gets its texture tier evaluated immediately — without waiting for the entity to appear in the frustum-culled visible set.

+

Entities tagged with TileLODTagComponent (HLOD and per-tile LOD proxy meshes) are skipped in this pass — they are transient geometry whose textures do not need progressive streaming.

+

The priorityEntities set is drained every tick and is also cleared by reset().

+
+

Priority Pass 1: Visible Entities

+

The system gets the current list of visible entity IDs (from the scene's frustum culling or visibility tracking) and iterates them first:

+
For each visible entity:
+  1. Calculate distance from camera to entity's world-space bounding box center
+  2. Determine desired tier via desiredMaxDimension(distance:)
+  3. Build work items — which textures actually need to change
+  4. If work items exist → scheduleResolutionChange(...)
+  5. Decrement available slots
+
+

City block example: The camera is standing on the sidewalk in front of Building #42. Buildings #42 and #43 are within 12m (full tier). Buildings #44–#60 are within 20m (medium tier). The remaining 440 buildings are beyond 20m (minimum tier).

+

On this tick, the three available slots might be assigned to: +- Building #42: upgrade base color from 1024px → full resolution (2048px) +- Building #43: upgrade roughness from 1024px → full resolution +- Building #57: already at 1024px medium — no change needed, slot freed

+
+

Priority Pass 2: Upgraded-but-Not-Visible Entities

+

After handling visible entities, the system checks its upgradedEntities set — entities whose textures are currently above the minimum tier. Even if they're off-screen now (the camera rotated away), they may still be nearby and deserve high-res textures so there's no quality drop when the camera rotates back.

+
For each entity in upgradedEntities that is NOT in the visible set:
+  1. Calculate actual distance (do not assume minimum)
+  2. Determine desired tier
+  3. Build work items
+  4. Schedule if needed
+
+

City block example: The camera rotated 90°, so Building #42 left the frustum. It is still 3m away. The system sees it in upgradedEntities, computes distance = 3m, desired tier = full — no downgrade is needed. It stays tracked.

+

If the camera then walks 20m away, Building #42's distance becomes 20m > 12m, so the system schedules a downgrade from full → 256px minimum.

+
+

Building Work Items

+

buildWorkItems(entityId:targetMaxDimension:) inspects every texture slot on the entity's meshes and filters to only the ones that actually need to change:

+
For each mesh → submesh → material:
+  For each texture type (baseColor, roughness, metallic, normal):
+    currentMax = max(currentTexture.width, currentTexture.height)
+    desiredMax = min(targetMaxDimension, sourceMaxDimension)
+
+    if currentMax == desiredMax → skip (already correct)
+    if upgrading but source is no bigger than current → skip
+
+    else → emit StreamWorkItem(slot, direction, targetMaxDimension)
+
+

Each StreamWorkItem carries: +- The mesh/submesh index to know where to write back +- The direction (.upgrade or .downgrade) +- The target max dimension (nil means full source resolution) +- The texture source: either an MDLTexture object or a URL on disk

+
+

Scheduling: The Async Task

+

scheduleResolutionChange(...) is where the real work happens — but critically it happens off the main thread:

+
1. reserveOp(entityId) — mark entity as busy, return false if already active
+2. Initialize MTLCommandQueue and MTKTextureLoader once (reused across ticks)
+3. Spawn a Swift Task (async, off main thread)
+
+

Inside the Task, for each work item:

+

Upgrade Path

+
loadSourceTexture(source, isSRGB:, loader:)
+  └─ MTKTextureLoader loads the original MDLTexture or URL from disk
+  └─ options: shaderRead | pixelFormatView, generateMipmaps: true, SRGB flag
+  └─ Returns a full-resolution MTLTexture
+
+resampleTextureIfNeeded(sourceTexture, targetMaxDimension:, commandQueue:)
+  └─ if targetMaxDimension == nil → return texture as-is (full res)
+  └─ else → GPU downsample to targetMaxDimension
+
+

Downgrade Path

+
resampleTextureIfNeeded(currentTexture, targetMaxDimension:, commandQueue:)
+  └─ GPU downsample the already-loaded texture to targetMaxDimension
+  └─ No disk I/O needed — current texture is the source
+
+

GPU Resampling (downsampleTexture)

+
1. Compute target dimensions preserving aspect ratio
+2. Allocate new MTLTexture (private storage, mipmapped)
+3. Encode MPSImageBilinearScale → bilinear downsample
+4. Encode BlitCommandEncoder.generateMipmaps(for:)
+5. commit() and await completion via CheckedContinuation
+
+

Using MPSImageBilinearScale means the downsampled texture is high quality (bilinear filtering by the GPU shader), and the full mip chain is generated immediately so the renderer can use the appropriate mip level right away.

+
+

Applying Results Back to ECS

+

After all textures in the task are loaded/resampled, execution returns to the main thread via await MainActor.run { withWorldMutationGate { ... } }:

+
For each LoadedTexture:
+  1. textureViewMatchingSRGB(texture, wantSRGB:)
+     └─ creates a MTLTextureView with sRGB or linear pixel format
+        without copying pixel data (zero cost)
+
+  2. updateMaterial(entityId:meshIndex:submeshIndex:) { material in
+       // Three-tier level: .full (nil cap), .capped (medium), .minimum
+       let streamLevel: TextureStreamingLevel = item.targetMaxDimension == nil
+           ? .full
+           : (item.targetMaxDimension! <= capturedMinimumDim ? .minimum : .capped)
+       material.baseColor.texture = item.texture
+       material.baseColorStreamingLevel = streamLevel
+       // (same for roughness, metallic, normal)
+     }
+
+  3. BatchingSystem.shared.updateBatchMaterialInPlace(for: entityId) { batchMaterial in
+       // Mirror the same three-tier level into the batch group's representative
+       // material so the new texture is visible on the next frame with zero batch churn
+     }
+
+

The withWorldMutationGate wrapper ensures the ECS is not mutated mid-render. The BatchingSystem update ensures batched draw calls reflect the new texture without rebuilding the batch.

+

After applying, the entity's membership in upgradedEntities is updated: if any texture is still above the minimum tier, the entity stays tracked.

+
+

The sRGB View

+

textureViewMatchingSRGB handles a subtle correctness issue: after GPU resampling, the output texture may have a linear pixel format even if the original was sRGB (e.g., rgba8Unorm instead of rgba8Unorm_srgb). Rather than re-encoding with the correct format, the system creates a MTLTextureView — a zero-copy reinterpretation of the same underlying memory with the correct format. This costs nothing in GPU memory.

+
+

Full Walk-Through: Building #42 Goes from Far to Near

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
EventAction
Scene loadsAll 500 buildings loaded at 256px (minimum tier) by TextureLoader
Camera 30m away from Building #42distance > 20m → already at minimum, no work
Camera walks to 18m awaydistance 18m → desired = 1024px medium; upgrade scheduled
Upgrade task runsLoads MDLTexture from source → 1024px; applied to ECS + batch
Building #42 added to upgradedEntities(1024px > minimum)
Camera walks to 10m awaydistance 10m ≤ 12m → desired = nil (full); upgrade scheduled
Upgrade task runsLoads MDLTexture → full 2048px, no GPU resample needed; applied
Camera walks away to 15mdistance 15m > 12m × (1 + 0.15) → downgrade to 1024px; scheduled
Downgrade task runsGPU resamples 2048px → 1024px; applied
Camera walks away to 25mdistance 25m > 20m × (1 + 0.15) → downgrade to 256px minimum; scheduled
Downgrade task runsGPU resamples 1024px → 256px; applied; entity removed from tracking
+

At no point are more than 3 buildings being streamed simultaneously, keeping GPU command submission predictable.

+
+

Threading Model

+ + + + + + + + + + + + + + + + + + + + + +
ThreadWhat happens there
Main / game loopupdate() called; distance math; buildWorkItems; reserveOp; resource init
Swift Task (async)Disk I/O (MTKTextureLoader); GPU encode + await (MPSImageBilinearScale)
MainActorECS mutation (updateMaterial); batch update; upgradedEntities bookkeeping
+

activeOps and upgradedEntities are protected by NSLock. The command queue and texture loader are initialized once on the main thread before any Task is spawned, then captured as local constants — no concurrent access to instance state from async tasks.

+
+

Memory Relief: shedTextureMemory

+

TextureStreamingSystem exposes a public method for on-demand texture downgrade under memory pressure:

+
@discardableResult
+public func shedTextureMemory(cameraPosition: simd_float3, maxEntities: Int = 4) -> Int
+
+

This is called by GeometryStreamingSystem — not on a timer, but reactively whenever combined GPU memory (mesh + texture) hits the 85% high-water mark. It bypasses the normal distance-band schedule and forces immediate action.

+

What it does: +1. Snapshots upgradedEntities — the set of entities currently holding textures above minimumTextureDimension +2. Calculates the camera distance for each +3. Sorts farthest-first — the least visually valuable textures at their current resolution get downgraded first +4. Schedules up to maxEntities force-downgrades to minimumTextureDimension, skipping any entity already in an active op +5. Returns the number of entities scheduled

+

Why farthest-first? A distant entity's 1024 px texture dropping to 256 px is nearly invisible. A nearby entity's texture downgrading would be immediately obvious. This ordering gives the maximum memory relief for the minimum perceptible quality loss.

+

Relationship to the update loop: Normal update() ticks also schedule downgrades for out-of-range entities, but only as slots become available and on the 0.2 s timer. shedTextureMemory is a burst — it fills up to maxEntities slots immediately, regardless of the timer, to respond to pressure before the next geometry load attempt.

+

When it is called

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CallermaxEntitiesCondition
GeometryStreamingSystem.update()4Combined pressure high, geometry pressure low — texture relief only, no geometry eviction
GeometryStreamingSystem.update()8Geometry pressure also high — shed texture first, then evict geometry
OS .warning pressure callback8MemoryBudgetManager.onMemoryPressureWarning fires — proactive shed before OS escalates
OS .critical pressure callback20MemoryBudgetManager.onMemoryPressureCritical fires — aggressive shed + double geometry eviction pass (16 evictions each) + CPU heap release via ProgressiveAssetLoader.releaseWarmAsset()
+

The larger batch size (8) when geometry is also under pressure reflects that more aggressive texture shedding is needed before the costlier geometry eviction path runs. The OS pressure rows bypass the normal per-tick budget check entirely — they fire out-of-band whenever the OS signals memory pressure, and the actual shedding runs on the next GeometryStreamingSystem.update() tick (deferred via a flag to stay on the main thread).

+
+

Tuning Profiles

+

Apply a built-in profile at scene init instead of setting every property individually:

+
TextureStreamingSystem.shared.apply(.detailed)   // close-inspection / high-detail assets
+TextureStreamingSystem.shared.apply(.superdetailed) // hero assets / showroom inspection
+TextureStreamingSystem.shared.apply(.openWorld)  // large outdoor scenes
+TextureStreamingSystem.shared.apply(.balanced)   // general-purpose default
+TextureStreamingSystem.shared.apply(.tiled)      // tile-based streaming (see alignToManifest)
+
+

Individual properties can be overridden after applying a profile:

+
TextureStreamingSystem.shared.apply(.detailed)
+TextureStreamingSystem.shared.upgradeRadius = 3.0  // widen full-res zone
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProfileupgradeRadiusdowngradeRadiusminDimmaxConcurrentOpsBest for
.detailed2.5 m6.0 m512 px6Vehicles, products, characters, props, interiors
.superdetailed4.0 m8.0 m1024 px6Showroom vehicles, product configurators, hero assets
.openWorld15.0 m60.0 m256 px3Cities, landscapes, terrain
.balanced12.0 m20.0 mplatform default3Mixed / unknown scene type
.tiled30.0 m*70.0 m*256 px6Tile-based streaming scenes
+

* Placeholder defaults only — immediately overridden by alignToManifest (see below).

+

Detailed rationale: close-inspection content keeps nearby surfaces large on screen, whether the subject is a car, product, character, prop, or room. The minimum tier is raised to 512 px (from the engine default of 256 px) because low-resolution mips look visibly compressed at a few metres. maxConcurrentOps = 6 is safe here because these streaming ops are GPU-bound (no cold disk I/O on the warm path). .archviz remains available as a deprecated compatibility alias for .detailed.

+

Superdetailed rationale: hero assets may be inspected from a few metres away while still filling a large portion of the view. This profile keeps full-resolution textures through 4 m, uses a 2048 px medium tier, and only drops to 1024 px beyond 8 m. Use compressed formats such as ASTC for large source textures before enabling this profile on memory-constrained devices.

+

Open-world rationale: tiers are spread across a city-block scale. The minimum tier stays at 256 px because objects beyond 60 m occupy very few pixels. Keeping maxConcurrentOps = 3 avoids GPU memory spikes when hundreds of entities enter range simultaneously.

+

Tiled rationale: tile streaming scenes have manifest-defined streaming and unload radii that vary per-scene. The .tiled profile sets concurrency (maxConcurrentOps = 6) and quality (minimumTextureDimension = 256, maxTextureDimension = 1024) for tile-scale geometry, then alignToManifest overrides the radii with values derived from the manifest's streaming_defaults.

+
+

Tile Streaming Integration

+

alignToManifest

+

When setEntityStreamScene() decodes a tile manifest, it calls:

+
TextureStreamingSystem.shared.alignToManifest(
+    streamingRadius: manifest.streamingDefaults.streamingRadius,
+    unloadRadius: manifest.streamingDefaults.unloadRadius
+)
+
+

This applies the .tiled profile (concurrency, dimensions) then derives the texture tier radii from the manifest geometry streaming bands:

+
upgradeRadius   = streamingRadius × 0.70
+downgradeRadius = max(unloadRadius, upgradeRadius + 1.0)
+
+

Why 0.70 × streamingRadius? The streaming radius is the distance at which tile geometry loads. Upgrading to full-res at 70% of that radius means the camera has already moved well inside the loaded zone before the texture upgrade fires — reducing the chance of a visible resolution pop the moment a tile appears.

+

Why downgradeRadius = unloadRadius? Tile geometry unloads at unloadRadius. Degrading textures to minimum at the same distance means the GPU texture memory is already at minimum cost exactly when the geometry is about to be evicted, preventing a spike of full/medium-res textures on geometry that is about to disappear.

+

Example (city.json: streaming_radius = 38.5m, unload_radius = 57.8m): +- upgradeRadius = 38.5 × 0.70 = 26.97m — full res within ~one tile diagonal +- downgradeRadius = 57.8m — minimum tier at the tile unload boundary

+

notifyEntitiesReady / cancelEntities

+

GeometryStreamingSystem informs the texture system about tile lifecycle events:

+
// Tile finished loading — schedule priority texture evaluation
+TextureStreamingSystem.shared.notifyEntitiesReady(tileRenderIds)
+
+// Tile unloading — cancel any in-flight or queued ops for its entities
+TextureStreamingSystem.shared.cancelEntities(renderIds)
+
+

cancelEntities removes the entity IDs from upgradedEntities, activeOps, and priorityEntities, ensuring no stale texture upgrade lands on a destroyed entity.

+
+

Key Configuration

+
TextureStreamingSystem.shared.upgradeRadius = 4.0      // meters: go full-res inside this
+TextureStreamingSystem.shared.downgradeRadius = 12.0   // meters: go minimum beyond this
+TextureStreamingSystem.shared.maxTextureDimension = 1024
+TextureStreamingSystem.shared.minimumTextureDimension = 256
+TextureStreamingSystem.shared.updateInterval = 0.2     // seconds between evaluations
+TextureStreamingSystem.shared.maxConcurrentOps = 3     // parallel streaming tasks
+TextureStreamingSystem.shared.hysteresisFraction = 0.15 // dead-band fraction at tier boundaries
+TextureStreamingSystem.shared.verboseLogging = true    // log each up/downgrade
+
+
+

Hysteresis Dead Band

+

Without hysteresis, an entity hovering exactly at a tier boundary (e.g. downgradeRadius = 20 m) oscillates between tiers on alternate streaming ticks, causing mip-map flicker on distant meshes.

+

hysteresisFraction (default 0.15) applies an asymmetric dead band at each tier boundary:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
TransitionTriggers at
Upgrade to full (boundary: upgradeRadius)distance < upgradeRadius × (1 − h)
Downgrade from fulldistance > upgradeRadius × (1 + h)
Upgrade to medium (boundary: downgradeRadius)distance < downgradeRadius × (1 − h)
Downgrade to minimumdistance > downgradeRadius × (1 + h)
+

At defaults (upgradeRadius = 12 m, downgradeRadius = 20 m, h = 0.15): +- Full ↔ medium transition: upgrade at < 10.2 m, downgrade at > 13.8 m +- Medium ↔ minimum transition: upgrade at < 17 m, downgrade at > 23 m

+

shedTextureMemory always bypasses hysteresis (passes Float.greatestFiniteMagnitude as distance) so memory-pressure downgrades are never suppressed.

+
+

ASTC Texture Pipeline

+

The engine ships a native ASTC texture loader (NativeTexFormat.swift, NativeTextureLoader.swift) that decodes ASTC-compressed textures stored inside .untold binary asset files without going through ModelIO or MTKTextureLoader.

+

How it interacts with texture streaming:

+
    +
  • ASTC textures embedded in .untold files are decoded by NativeTextureLoader at load time and handed to the same TextureLoader GPU cache used by the USDZ/USDC path. From the streaming system's perspective, they are ordinary MTLTexture objects.
  • +
  • All three streaming tiers (full, medium, minimum) apply normally — the system GPU-resamples the decoded ASTC texture to the target tier dimension using MPSImageBilinearScale, just as it does for PNG or JPEG source textures.
  • +
  • ASTC textures are typically much smaller on disk than uncompressed equivalents, so they reduce download time for remote tile assets and reduce ProgressiveAssetLoader CPU heap pressure.
  • +
  • The .superdetailed and .detailed streaming profiles note ASTC as the recommended source format for high-resolution hero assets on memory-constrained devices.
  • +
+

Format support: The engine targets MTLPixelFormatASTC_*_LDR block sizes (4×4, 6×6, 8×8). HDR ASTC is not currently supported. Use the Blender export pipeline's ASTC conversion step to produce compatible .untold files.

+
+

Bootstrap Tier Alignment

+

TextureLoader.defaultMaxTextureDimension (set in Mesh.swift) is aligned to TextureStreamingSystem.platformDefaultMinimumTextureDimension:

+
    +
  • visionOS: 192 px
  • +
  • macOS / iOS: 256 px
  • +
+

This ensures every freshly loaded entity starts at the streaming system's minimum tier. The streaming system then only upgrades as the camera approaches — it never issues an immediate downgrade on a newly-loaded entity (which would have been visible as a resolution pop on the first frame the entity appeared).

+
+

TextureLoader Cache Key Design

+

TextureLoader (the private helper class in Mesh.swift) maintains a per-instance GPU texture cache keyed by TextureCacheKey(id: String, isSRGB: Bool). Two possible key strategies are used depending on what information ModelIO provides:

+

Priority 1 — Bracket-notation path (safe for deduplication)

+

When property.stringValue contains a parseable USDZ bracket path +(e.g. "file:///scene.usdz[0/floor_albedo.png]"), the key is: +usdz-embedded://0/floor_albedo.png

+

This is unique per physical embedded file. Two materials that reference the same embedded texture file correctly share one GPU texture via this key.

+

Priority 2 — Object identity (safe from collision)

+

When bracket notation is absent, the key is the MDLTexture object's memory address: +mdl-obj-<hex-pointer>

+

This is used because the name-based fallback (usdz-embedded://GameData/embedded_Basecolor_map) is not safe as a cache key: multiple genuinely different materials can map to the same synthetic name when their MDLTexture objects have an empty .name property. Using a shared cache entry for different physical textures causes some meshes to display the wrong texture on first load.

+

Sharing still works correctly: two code paths that hold a reference to the same MDLTexture object (same pointer) get the same cache key and share one GPU texture, which is the intended deduplication.

+

outputURL vs. cacheKeyURL

+

Both values use the same strategy: bracket URL when available, object-identity URL otherwise.

+ + + + + + + + + + + + + + + + + + + + +
FieldValueUsed by
cacheKeyURLBracket URL or object-identity URLGPU textureCache lookup only
outputURLmaterial.baseColorURLBracket URL or object-identity URLBatchingSystem.getMaterialHash, TextureStreamingSystem source reference
+

Why both use object identity when bracket notation is absent:

+

The name-based fallback (usdz-embedded://scene.usdz/embedded_Basecolor_map) is the same string for every unnamed texture from the same USDZ, regardless of its actual pixel content. BatchingSystem.normalizeTextureURL then strips the asset-scope host, collapsing all unnamed textures to the same token (usdz-embedded://embedded_Basecolor_map). This causes getMaterialHash to produce the same hash for entities with genuinely different textures, grouping them into one batch and rendering all of them with the first entity's GPU texture — the wrong texture on every other entity.

+

Using object identity for outputURL as well means: +- Same MDLTexture pointer → same physical texture → same material.baseColorURL → same batch hash → share a batch group ✓ +- Different MDLTexture pointers → different physical textures → different material.baseColorURL → different batch hash → separate batch groups ✓

+

The MDLAsset is kept alive in ProgressiveAssetLoader.rootAssetRefs for the entity's lifetime, so MDLTexture pointers are stable across warm eviction/re-upload cycles.

+

Diagnostic Logging

+

Set textureCacheLoggingEnabled = true before loading to trace every cache hit/miss:

+
// Enable before calling setEntityMeshAsync
+textureCacheLoggingEnabled = true
+
+

Each log line contains: HIT/MISS, cache key, key source (bracket / obj-identity(unnamed) / obj-identity(named-no-bracket)), MDLTexture pointer, texture name, map type, and isSRGB flag.

+

Before the fix: you would see HIT entries where the same key is reused across different MDLTexture object identities for base-color textures — the collision.

+

After the fix: each unnamed/no-bracket texture gets its own obj-identity key; HIT entries are only seen when the same MDLTexture object is referenced by multiple materials (correct sharing).

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/Architecture/tilebasedstreaming/index.html b/site/Architecture/tilebasedstreaming/index.html new file mode 100644 index 00000000..66bc9a13 --- /dev/null +++ b/site/Architecture/tilebasedstreaming/index.html @@ -0,0 +1,3654 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tile-Based Streaming - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + + + + + + + +

UntoldEngine Tile-Based Streaming Architecture

+

Overview

+

UntoldEngine implements a multi-tier proximity-based geometry streaming system for large outdoor and indoor scenes on Apple platforms (macOS, visionOS). The system streams geometry in and out of GPU memory based on camera distance, using a spatial octree for efficient runtime range queries. Tile spatial partitioning in the manifest can follow either a uniform grid (v3) or a quadtree floor layout (v4) — see Manifest Versions and Quadtree Partitioning.

+

Tier 1 — Tile streaming (TileComponent): coarse-grained. Each tile is a whole USDC file covering a bounded region of the world. Tiles load and unload as the camera moves through the scene.

+

Tier 2 — OCC mesh streaming (StreamingComponent): fine-grained. Inside a loaded tile, individual mesh stubs upload to the GPU incrementally, governed by distance bands and memory budgets.

+

HLOD (Hierarchical Level of Detail): a coarse proxy mesh shown in place of an unloaded tile. Loaded when the tile leaves range, unloaded when the full tile finishes parsing. Provides continuity for distant geometry without holding the full tile in GPU memory.

+

Per-tile LOD levels (TileLODLevel): intermediate mesh representations that bridge the gap between the full tile (streamingRadius) and the HLOD switch distance. Finer than HLOD but coarser than the full tile; shown while the tile itself is unloaded and the camera is at mid-range.

+

Hysteresis: both HLOD and per-tile LOD transitions use a hysteresis band to prevent thrashing when the camera hovers near a switch boundary. A loaded representation is not unloaded until the camera moves meaningfully past the switch threshold (controlled by hlodHysteresisFactor and lodHysteresisFactor, both default 0.90 = 10% inner band). Without hysteresis, frame-to-frame distance jitter near a boundary causes rapid load/unload cycles that freeze the engine.

+

Distance Band Summary

+
camera distance →  close        mid-range       far         very far
+                   ──────────────────────────────────────────────────
+full tile          ████████████
+per-tile LOD 0                  ██████
+per-tile LOD 1                          ██████
+HLOD                                            ████████████████████
+nothing visible    nothing                                  (if no HLOD)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BandRepresentationControlled by
< streamingRadiusFull tile (all submeshes)TileComponent.state == .parsed
streamingRadius … LOD[n].switchDistancePer-tile LOD n (coarser each step)TileLODLevel.state
> last LOD switchDistance … hlodSwitchDistanceHLOD proxy meshTileComponent.hlodState
> hlodSwitchDistanceNothing (tile + HLOD both unloaded)
+
+

Data Model

+

Manifest (JSON)

+

A scene is described by a manifest file listing tiles.

+

Top-level manifest fields

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
versionInteger schema version (3 = uniform grid, 4 = quadtree floor)
partitioning_mode(v4 only) "uniform_grid" or "quadtree_floor" — describes how tiles were partitioned by the export pipeline
streaming_defaultsScene-wide fallback radii and priority used when a tile omits its own values
tilesArray of tile entries (see below)
shared_bucket(optional) A single always-resident tile for geometry that spans many tiles
tile_size(optional) Tile footprint in world units, used to align batch cell size with tile boundaries
interior_zone(v4 only) Union AABB of all ExteriorShell tiles. Interior tiles are only loaded while the camera is inside this volume
+

The streaming_defaults block sets scene-wide fallback values for all per-tile fields. An optional shared_bucket entry holds geometry that spans many tiles and should always be resident (loaded as soon as the camera enters the scene).

+

Per-tile entry fields

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
tile_idHuman-readable name (e.g. "tile_3_2")
path_relative_to_manifestPath to the USDC file, relative to the manifest
file_size_bytesPre-computed file size used by the memory budget gate
bounds.min / bounds.maxWorld-space AABB used for octree insertion and frustum tests
centerWorld-space center (used for distance calculations)
streaming_radius(optional) Per-tile load threshold; falls back to streaming_defaults
unload_radius(optional) Per-tile unload threshold; falls back to streaming_defaults
prefetch_radius(optional) Per-tile prefetch start; falls back to streaming_defaults, then auto
priority(optional) Load order when multiple tiles are candidates
hlod_levels(optional) Array of HLOD proxy entries; see HLOD
lod_levels(optional) Array of per-tile intermediate LOD entries; see Per-tile LOD Levels
floor_id(v4 only, optional) Floor index within a building; 0 = ground floor
quadtree_node_id(v4 only, optional) Quadtree node identifier written by the export script (e.g. "F02Q100"); used for debug logging only, not required for streaming
semantic_tier(v4 only, optional) One of "ExteriorShell", "StructuralInterior", "RoomContents", "FineProps". The streaming_radius already encodes the correct load distance for the tier; no additional runtime logic is required
interior(v4 only, optional) When true, this tile contains interior-only geometry and is gated on the camera being inside interior_zone
+

HLOD manifest contract

+
"hlod_levels": [
+  { "path": "tiles/tile_0_0_hlod.usdc", "switch_distance": 300.0 }
+]
+
+

switch_distance is the camera distance beyond which the HLOD is shown in place of the tile. Typically a single entry per tile. The HLOD file is a coarse merged mesh exported by the content pipeline.

+

Per-tile LOD manifest contract

+
"lod_levels": [
+  { "path": "tiles/tile_0_0_lod1.usdc", "switch_distance": 80.0 },
+  { "path": "tiles/tile_0_0_lod2.usdc", "switch_distance": 150.0 }
+]
+
+

Entries must be sorted ascending by switch_distance (smallest = finest = closest). switch_distance is the camera distance beyond which this LOD is preferred over the finer level (or the full tile). The engine sorts entries on load, so unsorted manifests are corrected at runtime, but sorted manifests are the canonical contract.

+

ECS Components

+
    +
  • TileComponent — attached to every tile stub entity created by setEntityStreamScene(). Carries all metadata needed for the streaming bootstrap and teardown lifecycle. Key fields added for HLOD and LOD:
  • +
  • hlodURL, hlodEntityId, hlodState, hlodSwitchDistance, hlodLoadTask — HLOD lifecycle
  • +
  • lodLevels: [TileLODLevel] — per-tile intermediate LOD entries
  • +
  • meshEntityId — the dedicated mesh-child entity ID, stored so the timeout guard can force-close AssetLoadingGate if loadTextures() hangs
  • +
  • TileLODLevel — one instance per LOD entry in the manifest. Carries url, switchDistance, entityId, state (HLODAssetState), and loadTask. Mirrors the HLOD lifecycle pattern.
  • +
  • TileLODTagComponent — lightweight tag placed on render-descendant mesh entities spawned by the streaming system for per-tile LOD levels and HLODs. Carries a levelIndex used by the LOD debug renderer (colorRenderablesByLOD) and by BatchingSystem.resolveBatchCandidate to derive the batch LOD index for these entities (which have no LODComponent). levelIndex follows lodDebugPalette: 1 = LOD1 (green), 2 = LOD2 (blue), 5 = HLOD (cyan).
  • +
  • StreamingComponent — attached to individual OCC mesh stubs created inside a loaded tile. Governs per-mesh load/unload within the second streaming tier.
  • +
  • RenderComponent — added to an entity only after its GPU geometry upload completes. Absence means the entity is invisible to culling and rendering.
  • +
+
+

Manifest Versions and Quadtree Partitioning

+

The manifest schema has evolved across two versions:

+

v3 — Uniform Grid

+

"partitioning_mode": "uniform_grid" (or version: 3 without the field). Tiles are laid out in a regular spatial grid. There is no interior_zone and no semantic tier hierarchy. This is the original format produced by the v1 Blender export script.

+

v4 — Quadtree Floor

+

"partitioning_mode": "quadtree_floor" (or version: 4). Tiles are partitioned by a floor-level quadtree, typically for multi-storey indoor scenes. The export script assigns each tile a quadtree_node_id (e.g. "F02Q100") and a semantic_tier label.

+

Semantic tiers encode the expected load distance by naming convention — the export pipeline sets streaming_radius to the correct value for each tier, so the runtime treats them identically during streaming. The tiers are:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
TierDescription
ExteriorShellOuter building shell, always-visible facade geometry
StructuralInteriorFloors, walls, and structural elements inside the shell
RoomContentsFurniture and fixtures within individual rooms
FinePropsSmall detail props, only visible at close range
+

Interior zone gating — the manifest's interior_zone is the union AABB of all ExteriorShell tiles. On each streaming tick, the engine checks whether the camera is inside this volume. Tiles with "interior": true are only dispatched for loading while the camera is inside. This prevents the engine from loading room-level geometry when the player is outside the building, regardless of distance.

+
+

The quadtree partitioning is a content-pipeline and manifest-level concept. At runtime the engine uses an octree for spatial range queries (finding tile stubs near the camera). The manifest's quadtree_node_id is used for debug logging only and has no effect on streaming logic.

+
+
+

Tile Lifecycle

+

States

+
unloaded → parsing → parsed → unloading → unloaded
+                  ↘ failed → (retry backoff) → unloaded
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StateMeaning
.unloadedStub registered; no geometry in flight
.parsingsetEntityMeshAsync Task is running; GPU upload in progress
.parsedTile's child entities exist and are rendering (or uploading via OCC)
.failedLast parse attempt failed; exponential backoff before retry (5 s → 10 s → 20 s → max 60 s)
.unloadingTeardown in progress; blocks re-dispatch for this tick
+

1. Scene Load (setEntityStreamScene)

+
    +
  1. Locates and decodes the manifest JSON (no geometry parsed). If the manifest URL is HTTP/HTTPS, it is downloaded and cached via RemoteAssetDownloader before decoding. Tile asset URLs in the manifest are resolved relative to the manifest's base URL, so remote manifests produce remote tile URLs (e.g. https://cdn.example.com/scene/tiles/tile_0_0.untold). See asset_remote_streaming.md for the full download lifecycle.
  2. +
  3. Resets interiorZone and firstRangeTimestamps on GeometryStreamingSystem so stale scene-level state from a previous scene does not bleed into the new one.
  4. +
  5. Registers the supplied root entity with TiledSceneComponent, LocalTransformComponent, and ScenegraphComponent.
  6. +
  7. Registers one lightweight stub entity per tile inside a single withWorldMutationGate, parented under the root entity. Each stub receives:
  8. +
  9. Identity world transform
  10. +
  11. LocalTransformComponent.boundingBox set to the tile's world-space AABB
  12. +
  13. TileComponent in .unloaded state, with all radii and metadata from the manifest
  14. +
  15. Octree registration (so queryNear finds it immediately)
  16. +
+

No geometry is parsed or uploaded at this stage. The whole function completes in milliseconds regardless of scene size.

+

2. Streaming Update (per tick, ~100 ms steady / ~16 ms burst)

+

GeometryStreamingSystem.update(cameraPosition:deltaTime:) runs each frame. It queries the octree for all entities within maxQueryRadius (default 500 m). This is a query ceiling, not a load radius — it just defines the outer bound of the candidate pool. Each entity in the result is then tested against its own per-entity radii (effectivePrefetchRadius, streamingRadius, unloadRadius) to decide what actually happens. maxQueryRadius must be large enough to cover the largest unloadRadius in the scene, or far tiles will never be found for out-of-range teardown.

+

Camera velocity is computed each tick via exponential smoothing and used to project a predictive position (velocityLookAheadTime = 0.5 s ahead). Tile distances are scored against min(actual, predictive) so tiles in the direction of travel are prioritised before the camera physically arrives.

+

Tile load pass — for each .unloaded stub:

+
    +
  1. Computes effective distance using the predictive position.
  2. +
  3. Tests against effectivePrefetchRadius (see Prefetch Radius).
  4. +
  5. Applies the frustum gate (padded AABB vs camera frustum, tileFrustumGatePadding = 20 m). Tiles fully outside the frustum are skipped this tick.
  6. +
  7. Eligible tiles are sorted by priority (descending) then distance (ascending).
  8. +
  9. Up to maxConcurrentTileLoads (default 2) are dispatched via loadTile(), subject to the memory budget gate: the total parse memory in flight must stay under tileParseMemoryBudgetMB (200 MB), with a guarantee that at least one tile always loads even if it alone exceeds the budget.
  10. +
+

Tile unload pass — three sub-passes each tick. All passes use min(actual, predictive) distance, matching the load pass, so a tile the camera is approaching is not torn down mid-parse:

+
    +
  1. Nearby tiles still in the octree result but beyond unloadRadius.
  2. +
  3. Loaded tiles that drifted entirely outside maxQueryRadius.
  4. +
  5. Parsing tiles that drifted outside maxQueryRadius (fast movement or teleport).
  6. +
+

Both .parsing and .parsed tiles go through the grace period (see Unload Grace Period) before actual teardown (passes 1 and 2). Pass 3 tiles are genuinely beyond the 500 m query radius and are cancelled without a grace period — boundary oscillation cannot occur at that range. At most maxTileUnloadsPerUpdate (default 2) tiles are torn down per tick to spread GPU buffer releases across frames.

+

3. loadTile(entityId:)

+
    +
  1. Sets tileComp.state = .parsing; reserves a slot in activeTileLoads.
  2. +
  3. Creates a dedicated child mesh entity under the tile stub (capturedMeshEntityId) inside withWorldMutationGate. This guarantees unloadTile's collectTileDescendants always has at least one child to destroy, regardless of how many submeshes the tile contains.
  4. +
  5. Registers capturedMeshEntityId → tileEntityId in meshEntityToTileEntity for O(1) OCC upload counter updates.
  6. +
  7. Spawns a Swift Task calling setEntityMeshAsync(entityId: capturedMeshEntityId, streamingPolicy: .auto, blockRenderLoop: false).
  8. +
  9. .auto policy: the admission gate chooses fullLoad (parse + immediate GPU upload) or outOfCore (parse to CPU heap, upload stubs via StreamingComponent) based on tile file size and available RAM.
  10. +
  11. blockRenderLoop: false — tile parses do not hold the AssetLoadingGate open. Without this, concurrent tile parses would keep isLoadingAny == true for their full duration, freezing visibleEntityIds updates and stalling the render loop. LOD and HLOD loads also use blockRenderLoop: false for the same reason.
  12. +
  13. Completion callback (fires on the main thread):
  14. +
  15. Zombie-state guard — checks tc.state == .parsing. If unloadTile ran while the parse was in flight, the state will be .unloading. The callback discards the result, destroys the pre-created child entity, and returns without marking the tile loaded.
  16. +
  17. On confirmed .parsing: transitions to .parsed, seeds totalOCCStubs from countOCCDescendants.
  18. +
  19. fullLoad path (occCount == 0): all geometry is immediately GPU-resident. The callback:
      +
    1. Calls setEntityStaticBatchComponent to tag the entity hierarchy for cell-based static batching.
    2. +
    3. Calls BatchingSystem.shared.notifyTileEntitiesResident(_:) with the set of render descendant IDs. This single call replaces the former two-step queueResidencyEventsForRenderDescendants + notifyTileParsedEntities pairing — it directly registers the entities in the batching system's pending additions and marks them for quiescence bypass, avoiding the per-entity event storm through SystemEventBus. See Tile-Local Batch Promotion.
    4. +
    +
  20. +
  21. OCC path (occCount > 0): setEntityStaticBatchComponent is called but residency notifications are not sent — they fire automatically as each OCC stub completes its GPU upload via the normal handleResidencyChange flow. The normal quiescence delay applies to keep the batch from rebuilding after each individual stub upload.
  22. +
  23. On failure: destroys child entity, increments failureCount, sets state to .failed (retry backoff).
  24. +
  25. defer { releaseActiveTileLoad } — the concurrency slot is freed on all exit paths.
  26. +
+

4. OCC Sub-Mesh Upload (second streaming tier)

+

For large tiles using the outOfCore path, setEntityMeshAsync creates child OCC stub entities under capturedMeshEntityId, each with a StreamingComponent in .unloaded state. GeometryStreamingSystem.update() iterates these stubs in a separate pass — uploading them in batches governed by maxConcurrentLoads (3), with a nearBandMaxConcurrentLoads = 1 serial slot for the closest mesh stubs so distance-ordered appearance is preserved.

+

Each completed OCC upload calls incrementParentTileOCCCount(for:), which increments tileComp.uploadedOCCStubs. The visualState property (TileVisualState) tracks upload progress: .empty.partial.usable (≥ 50% uploaded) → .complete.

+

5. unloadTile(entityId:)

+
    +
  1. Captures wasParsing = (tileComp.state == .parsing).
  2. +
  3. Sets tileComp.state = .unloading; cancels tileComp.loadTask.
  4. +
  5. If wasParsing: removes from loadingTileEntities and bails out. The Task completion callback will find .unloading, discard the result, and dispatch deferred child-entity cleanup — this avoids a concurrent ECS write race since setEntityMeshAsync may still be running.
  6. +
  7. If .parsed: calls collectTileDescendants(entityId) to walk the child tree, cancelling any in-flight OCC streaming tasks. Calls destroyEntity on all descendants + finalizePendingDestroys(). This releases GPU buffers, removes octree entries, releases MeshResourceManager refs, and unregisters from MemoryBudgetManager.
  8. +
  9. Calls ProgressiveAssetLoader.shared.removeOutOfCoreAsset(rootEntityId:) to free CPU-heap MDLAsset data for out-of-core tiles.
  10. +
  11. Resets totalOCCStubs, uploadedOCCStubs, pendingUnloadSince to 0.
  12. +
  13. Sets tileComp.state = .unloaded; removes from loadedTileEntities.
  14. +
+

The tile stub entity itself is never destroyed. It stays in the octree as a cheap placeholder so the streaming system reloads the tile on the next approach.

+
+

Prefetch Radius

+

The prefetch radius decouples "when the tile starts loading" from "when the tile must be visible." Tiles begin parsing as soon as the camera enters effectivePrefetchRadius, which is larger than streamingRadius. By the time the camera reaches streamingRadius, the parse is already complete and the geometry appears without a blank frame.

+
                  camera direction →
+─────────────────────────────────────────────────────
+                  prefetchRadius (auto: midpoint)
+                  │           streamingRadius
+                  │           │        unloadRadius
+                  ▼           ▼        ▼
+  · · · · · · · ·|· · · · · ·|████████|· · · · · · ·
+                 start       tile is  stop
+                 loading     visible  loading
+
+

effectivePrefetchRadius resolution (in priority order): +1. Per-tile prefetch_radius field in the manifest +2. Scene-wide prefetch_radius in streaming_defaults +3. Auto: streamingRadius + (unloadRadius − streamingRadius) × 0.5

+

For the typical streamingRadius = 80 m, unloadRadius = 120 m default, auto resolves to 100 m — giving 20 m of prefetch advance at walking speed (~1.5 m/s) that is ~13 seconds of loading headroom, well above the 1–2 s parse time for a 15–20 MB tile.

+
+

Unload Grace Period

+

When a .parsed tile (with visible GPU geometry) first exceeds unloadRadius, pendingUnloadSince is set to the current time. The tile is only torn down once CFAbsoluteTimeGetCurrent() − pendingUnloadSince ≥ unloadGracePeriod (default 3 seconds).

+

If the camera re-enters unloadRadius before the grace period expires, pendingUnloadSince is reset to 0 and the tile stays loaded with no interruption. This eliminates rapid load/unload oscillation at tile boundaries (the most common cause of flickering at tile edges).

+

Both .parsing and .parsed tiles honour the grace period. .parsing tiles have no visible geometry, but the grace window lets an in-flight parse complete naturally rather than being cancelled and immediately re-dispatched. Immediate cancellation was a false economy: the cancelled Swift Task still ran to completion before the state could reset, so the tile was re-dispatched on the very next tick, creating a tight load-cancel loop.

+

pendingUnloadSince is also reset in unloadTile() so the counter is clean for the next load/unload cycle.

+
+

Memory Management

+
    +
  • MemoryBudgetManager tracks geometry and texture GPU bytes against per-platform budgets (probed at startup).
  • +
  • Before dispatching any tile load, the system checks shouldEvictGeometry(). If true, it runs TextureStreamingSystem.shedTextureMemory and evictLRU (capped at 8 evictions) before attempting a tile parse. This prevents a tile's multi-MB commit from pushing RAM over budget.
  • +
  • LRU eviction scores loaded streaming entities by camera distance × evictionDistanceWeight + GPU size × evictionSizeWeight. Entities within visibleEvictionProtectionRadius (30 m) are protected.
  • +
  • OS memory pressure callbacks (DispatchSource.makeMemoryPressureSource) set a flag; eviction is deferred to the next update() tick to stay single-threaded.
  • +
  • tripleVisibleEntities.clearAll() — called in finalizePendingDestroys() to clear all triple-buffer slots so the renderer does not read stale entity IDs after a scene reload.
  • +
+
+

Threading Model

+
    +
  • All ECS mutations (createEntity, registerComponent, destroyEntity, finalizePendingDestroys) must run on the main thread.
  • +
  • Background Swift Tasks handle disk I/O, USDC parsing, and CPU→Metal buffer copies.
  • +
  • withWorldMutationGate is an activity counter, not a mutex. It does not provide mutual exclusion — it signals that ECS mutations are occurring.
  • +
  • scene.exists(entityId) guards before every ECS write in upload completions prevent writes to entities destroyed while an upload was in flight (cooperative cancellation race).
  • +
  • Tile tracking sets (loadedTileEntities, loadingTileEntities, activeTileLoads, meshEntityToTileEntity) are protected by stateLock and accessed only through accessor methods.
  • +
+
+

Scene Reload Safety

+

When setEntityStreamScene is called for a second time (replacing a previous scene):

+
    +
  1. The caller destroys the old root entity (destroyEntity(entityId: oldRoot)), which cascades to all tile stubs. For each destroyed stub, removeTileComponent (the ComponentRegistry cleanup handler for TileComponent) cancels the tile's in-flight loadTask and calls GeometryStreamingSystem.shared.unregisterTileEntity(entityId), removing stale IDs from all tracking sets atomically.
  2. +
  3. setEntityStreamScene resets interiorZone and firstRangeTimestamps so scene-level streaming state is clean for the new scene.
  4. +
  5. New stubs are registered under the new root entity.
  6. +
+

Any tile Task that was already in flight and completes after step 1–2 finds scene.exists(entityId) == false and returns early via the guard in its completion closure. The defer-based slot release still fires, so no concurrency slot is leaked.

+
+
+

HLOD (Hierarchical Level of Detail)

+

HLOD fills the gap beyond a tile's unloadRadius by showing a coarse proxy mesh while the full tile is not in GPU memory. This eliminates the visual "pop to nothing" when a tile leaves range.

+

Lifecycle

+
hlodState:  unloaded → loading → loaded → unloading → unloaded
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
StateMeaning
.unloadedNo HLOD geometry in GPU memory
.loadingloadHLOD() task running
.loadedHLOD entity exists and is rendering
.unloadingTeardown in progress
+

Load trigger

+

HLOD is loaded when all of the following hold: +- The tile has an hlodURL in its TileComponent +- dist >= hlodSwitchDistance (camera is beyond the switch threshold) +- tileComp.state != .parsed (full tile is not resident — no redundant HLOD) +- hlodState == .unloaded +- activeHLODLoadCount() < maxConcurrentHLODLoads (default 4) — prevents simultaneous mass dispatch of 100+ HLOD parses that would OOM-kill the process

+

Unload trigger

+

HLOD is unloaded (and the load task cancelled) when: +- The full tile reaches .parsed — full geometry has taken over; HLOD is redundant. No hysteresis here — always unload promptly. +- dist < hlodSwitchDistance × hlodHysteresisFactor (default 0.90) — camera has moved meaningfully inside the switch distance. The hysteresis band [switchDistance × factor, switchDistance) keeps the HLOD resident while the camera lingers at the boundary, preventing thrashing.

+

Race fix: hlodState = .unloading is set before hlodLoadTask.cancel(). This prevents the load completion callback from seeing .loading and incorrectly marking the HLOD as .loaded after teardown is already in progress.

+

Minimum dwell guard: After any HLOD load or unload, tileComp.lastHLODTransitionTime is stamped. The next HLOD transition is suppressed until secondaryRepresentationMinDwellSeconds (default 1.0 s) has elapsed. This prevents the HLOD from flip-flopping faster than once per second when the camera lingers at the switch boundary while hysteresis alone is insufficient.

+

Out-of-range cleanup

+

A second pass after the main HLOD pass tears down HLOD entities for tiles that have drifted entirely outside maxQueryRadius. These tiles are no longer in the octree result and would otherwise remain with stale .loaded HLOD entities indefinitely.

+
+

Per-tile LOD Levels

+

Per-tile LODs fill the mid-distance band between the full tile (streamingRadius) and the HLOD (hlodSwitchDistance). They are intermediate meshes — coarser than the full tile but finer than HLOD — shown while the tile itself is unloaded.

+

Distinction from HLOD:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HLODPer-tile LOD
PurposeReplace unloaded tile at very far distancesProvide finer intermediate detail at mid distances
Memory concernYes — always-resident coarse meshYes — one entity per active LOD level
GPU cost concernLow (coarse mesh)Medium (finer than HLOD, less than full tile)
Distance band> hlodSwitchDistancestreamingRadius … hlodSwitchDistance
+

Lifecycle

+

Each TileLODLevel follows the same HLODAssetState state machine as HLOD:

+
state:  unloaded → loading → loaded → unloading → unloaded
+
+

LOD selection logic (per frame, per tile)

+
if HLOD is loaded or loading → unload all LOD levels (avoid dual representation)
+if dist >= hlodSwitchDistance → unload all LOD levels (HLOD pass handles this band)
+
+find target index (with hysteresis):
+  for each level i:
+    threshold = (i is currently active) ? switchDistance × lodHysteresisFactor : switchDistance
+    last level where threshold ≤ dist → that level
+  no match (dist < LOD[0].threshold)  → no LOD (tile will load or is already parsed)
+
+load target level if .unloaded (capped by maxConcurrentLODLoads)
+unload all other levels that are .loaded or .loading
+
+

The hysteresis ensures the currently active LOD level uses a lowered unload threshold (switchDistance × lodHysteresisFactor, default 0.90), so the camera must move meaningfully inward before the level is swapped. Levels that are not currently active use the full switchDistance.

+

Minimum dwell guard: LOD transitions are also gated by secondaryRepresentationMinDwellSeconds (default 1.0 s). After any per-tile LOD load or unload, tileComp.lastLODTransitionTime is stamped and the next transition is suppressed until 1.0 s has elapsed. This prevents oscillation between adjacent LOD levels when the camera is stationary near a switch threshold.

+

When tileComp.state == .parsed (full tile is resident), all LOD levels are unloaded — the full tile has taken over for that distance band.

+

loadLODLevel(entityId:levelIndex:)

+
    +
  1. Creates a child entity under the tile stub.
  2. +
  3. Loads the LOD USDC via setEntityMeshAsync with .immediate policy and blockRenderLoop: false (small proxy mesh, must not stall the render loop).
  4. +
  5. On success: tags render descendants with TileLODTagComponent(levelIndex: capturedIndex + 1) for LOD debug visualization, tags the entity for static batching (setEntityStaticBatchComponent), and calls BatchingSystem.shared.notifyTileEntitiesResident(_:) to bypass the quiescence delay.
  6. +
  7. Marks the entity in loadedLODEntities tracking set.
  8. +
+

unloadLODLevel(entityId:levelIndex:)

+
    +
  1. Sets level.state = .unloading before level.loadTask?.cancel() (same race fix as HLOD).
  2. +
  3. Calls BatchingSystem.shared.cancelPendingEntities(_:) with the render descendant IDs — removes them from all pending batching queues before the entities are destroyed, preventing "entity is missing" errors on the next batching tick.
  4. +
  5. Destroys the child entity.
  6. +
  7. Force-releases AssetLoadingGate for the destroyed entity (idempotent no-op if the Task already closed it).
  8. +
  9. Removes from loadedLODEntities when all levels are clear.
  10. +
+

Out-of-range cleanup

+

A pass after the LOD streaming pass tears down LOD entities for tiles outside maxQueryRadius, mirroring the HLOD cleanup pass.

+

Interaction with loadTile completion

+

When the full tile finishes parsing, unloadAllLODLevels(entityId:) is called alongside unloadHLOD(entityId:). Both intermediate representations are removed as the full tile takes over.

+
+

Asset Loading Freeze Prevention

+

ModelIO's loadTextures() is a blocking call that can hang indefinitely on unsupported image formats inside USDC archives (e.g. a format that stalls the ObjC image decoder with no timeout). When it hangs, AssetLoadingState.finishLoading is never called, AssetLoadingGate.isLoadingAny stays true permanently, and RenderingSystem skips all ECS traversal and culling every frame — the app remains alive but the view is frozen.

+

Fix: timeout guard + ResumeOnce

+

loadTextures() in RegistrationSystem is wrapped with a DispatchQueue + 15-second deadline:

+
let textureLoadOK = await withCheckedContinuation { cont in
+    let once = ResumeOnce()        // NSLock-backed: resumes cont exactly once
+    DispatchQueue.global(qos: .userInitiated).async {
+        assetRef.loadTextures()
+        once.callOnce { cont.resume(returning: true) }
+    }
+    DispatchQueue.global().asyncAfter(deadline: .now() + 15.0) {
+        once.callOnce { cont.resume(returning: false) }   // deadline fires
+    }
+}
+
+

If loadTextures() hangs, the deadline fires after 15 s and the async continuation proceeds without textures (geometry is still rendered, just untextured).

+

Force-closing the gate

+

TileComponent.meshEntityId stores the dedicated mesh-child entity ID. If the tile is unloaded while a parse is in flight and loadTextures() is hung, the timeout guard retrieves meshEntityId and force-closes the gate:

+
let hungMeshId = tc.meshEntityId
+tc.meshEntityId = .invalid
+if hungMeshId != .invalid {
+    Task { await AssetLoadingState.shared.finishLoading(entityId: hungMeshId) }
+}
+
+

This unblocks AssetLoadingGate, allowing the render loop to resume its normal ECS traversal.

+
+

Tile-Parse Watchdog

+

The loadTextures() 15-second timeout (above) guards against a hung texture decode inside an already-running parse. A separate, coarser watchdog guards against the entire tile parse Task becoming stuck — for example when the OS suspends the Task, disk I/O stalls indefinitely, or a remote download never completes.

+

On every streaming tick, GeometryStreamingSystem.update() checks every tile currently in .parsing state:

+
if CFAbsoluteTimeGetCurrent() - tc.parseStartTime > tileParseTimeoutSeconds (default 60 s):
+    cancel loadTask
+    force-close AssetLoadingGate for meshEntityId
+    increment failureCount → tile enters exponential backoff
+    release concurrency slot
+
+

parseStartTime is set after the remote fetch completes, not when loadTile() is first called. For remote tiles, the time waiting for the network does not count against the 60-second budget — only the actual CPU parse time does. This avoids spurious timeouts on slow connections.

+

Two distinct timeout layers:

+ + + + + + + + + + + + + + + + + + + + + + + +
MechanismScopeDeadlineOn trigger
ResumeOnce + DispatchQueueloadTextures() call only15 sProceeds without textures; geometry still renders
Tile-parse watchdogEntire tile parse Task60 s (tileParseTimeoutSeconds)Cancels task, marks .failed, enters retry backoff
+
+

Key Design Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyDefaultNotes
maxConcurrentTileLoads2Hard cap on simultaneous tile parses
maxConcurrentLODLoads4Hard cap on simultaneous per-tile LOD level loads
maxConcurrentHLODLoads4Hard cap on simultaneous HLOD mesh loads
lodHysteresisFactor0.90LOD unload threshold multiplier (10% inner band)
hlodHysteresisFactor0.90HLOD unload threshold multiplier (10% inner band)
tileParseMemoryBudgetMB200 MBTotal CPU parse memory allowed in flight
maxTileUnloadsPerUpdate2Max tile teardowns per streaming tick
unloadGracePeriod3.0 sHold time before tearing down a visible tile
maxConcurrentLoads (OCC)3Simultaneous mesh-level GPU uploads
nearBandMaxConcurrentLoads1Serial slot for closest mesh stubs
maxUnloadsPerUpdate (mesh)12Max mesh-level unloads per tick
updateInterval100 msStreaming tick rate (steady state)
burstTickInterval16 msTick rate during near-band backlog
frustumGatePadding (mesh)5 mFrustum pad for mesh-level candidates
tileFrustumGatePadding20 mFrustum pad for tile-level candidates
velocityLookAheadTime0.5 sPredictive position look-ahead
velocityLookAheadMinSpeed1.5 m/sMinimum speed to activate look-ahead
visibleEvictionProtectionRadius30 mDistance inside which eviction is blocked
hlodSwitchDistancemanifest switch_distanceCamera distance beyond which HLOD is shown
LOD switchDistancemanifest switch_distance per entryCamera distance beyond which this LOD is preferred
loadTextures() timeout15 sDeadline before ResumeOnce force-proceeds without textures
tileParseTimeoutSeconds60 sWatchdog deadline for an entire tile parse Task; forces tile to .failed and frees concurrency slot — distinct from the per-loadTextures() 15 s timeout
secondaryRepresentationMinDwellSeconds1.0 sMinimum time a HLOD or per-tile LOD level must dwell before the next transition is allowed; prevents flip-flopping between representations faster than once per second
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/Architecture/xrRenderingSystem/index.html b/site/Architecture/xrRenderingSystem/index.html new file mode 100644 index 00000000..d51ced69 --- /dev/null +++ b/site/Architecture/xrRenderingSystem/index.html @@ -0,0 +1,2591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + XR Rendering System - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

XR Rendering System — How It Works

+

The XR rendering system is the visionOS counterpart to UpdateRenderingSystem. It drives the same deferred render graph used on macOS, but within a completely different frame lifecycle imposed by CompositorServices — Apple's low-latency compositor for spatial computing.

+

The entry point is UntoldEngineXR, a class that owns the render loop, the ARKit session, and the spatial input bridge. Everything in this file is compiled only on visionOS (#if os(visionOS)).

+
+

Why XR Rendering Is Different

+

On macOS, the MTKView delegate calls draw(in:) on the main thread at display refresh rate. The engine owns the timing.

+

On visionOS, the compositor owns the timing. It tells you when to render, provides per-eye textures, and requires you to attach a device anchor (head pose) to every frame before presenting. If you miss the deadline or present without an anchor, the compositor either drops your frame or logs a warning. The UntoldEngineXR run loop is specifically structured to satisfy these compositor requirements frame by frame.

+
+

Step 0: Initialization

+
UntoldEngineXR(layerRenderer: LayerRenderer, device: MTLDevice)
+
+

At init time, three things happen in parallel:

+

ARKit session startup (async Task): Queries world sensing authorization, then launches WorldTrackingProvider and PlaneDetectionProvider. World tracking is what gives you the device anchor — the head pose needed to render correctly in the user's space. If world sensing is denied (e.g., the user blocked it in Settings), the engine still runs with world tracking only so rendering doesn't break, it just has no plane data.

+

Plane monitor (background Task): A long-running Swift structured concurrency task that consumes the planeDetection.anchorUpdates async stream. Every time the system detects, updates, or removes a real-world surface (floor, wall, table, etc.), it maps the ARKit classification to the engine's RealSurfaceKind enum and forwards it to RealSurfacePlaneStore. Game code queries this store to snap objects to real surfaces.

+

Renderer creation: UntoldRenderer.createXR(...) initializes the Metal device, command queue, G-Buffer textures, pipeline states, and all other GPU resources at the fixed visionOS viewport size (2048 × 1984 per eye).

+
+

Step 1: The Run Loop

+

runLoop() is called from a dedicated background thread (the compositor render thread) and runs for the lifetime of the XR session:

+
while true {
+    switch layerRenderer.state {
+    case .paused:    layerRenderer.waitUntilRunning()
+    case .running:   renderNewFrame()
+    case .invalidated: break  // exit
+    }
+}
+
+

The .paused state blocks the thread cheaply until the compositor is ready — this happens when the user puts the app in the background or when the system needs to reclaim resources. The .invalidated state is the clean shutdown signal.

+

Why a background thread? The compositor render thread must never be blocked by Swift's main actor or UIKit layout passes. Running here ensures that Metal encoding proceeds at compositor frame rate (90 FPS on Vision Pro) without contention.

+
+

Step 2: The Per-Frame Lifecycle — renderNewFrame()

+

CompositorServices frames follow a strict protocol. Every call to renderNewFrame() must progress through these phases in order:

+

2a. Query + Predict

+
let frame = layerRenderer.queryNextFrame()
+let timing = frame.predictTiming()
+
+

queryNextFrame() dequeues the next compositor frame. predictTiming() returns optimalInputTime — the deadline by which you must finish reading input and preparing CPU-side data for the frame. These are not suggestions; missing them causes judder.

+

2b. Update Phase

+
frame.startUpdate()
+// ... do CPU work ...
+frame.endUpdate()
+
+

Everything between startUpdate and endUpdate is CPU-side frame preparation:

+
    +
  • +

    Progressive loading tick: ProgressiveAssetLoader.shared.tick() is dispatched to the main thread via DispatchQueue.main.async. The run loop lives on the compositor thread, but tick() requires @MainActor. This mirrors what UntoldEngine.swift's draw() does on macOS.

    +
  • +
  • +

    Spatial input processing: updateSpatialInputState() drains the queued XRSpatialInputSnapshot events and updates the InputSystem. If assets are loading or the scene isn't ready, input is cleared instead to avoid acting on stale state.

    +
  • +
  • +

    Game update: renderer.updateXR() calls the user's gameUpdate and handleInput callbacks. This is where game logic runs — entity movement, animation state machines, physics steps. It is skipped entirely while AssetLoadingGate.shared.isLoadingAny is true.

    +
  • +
+

2c. Wait for Optimal Input Time

+
LayerRenderer.Clock().wait(until: timing.optimalInputTime, tolerance: .zero)
+
+

The thread sleeps until the compositor says it's the best moment to submit GPU work. Submitting too early wastes GPU time on a stale pose; submitting too late misses the scanline. This one call is what makes visionOS rendering feel low-latency.

+

2d. Submission Phase

+
frame.startSubmission()
+defer { frame.endSubmission() }
+
+

The defer is important: endSubmission() must be called even if rendering fails partway through. If it isn't called, the compositor stalls. The defer guarantees this regardless of how the function exits.

+

2e. Device Anchor Acquisition

+
let deviceAnchor = worldTracking.queryDeviceAnchor(atTimestamp: presentationTimeCA)
+drawable.deviceAnchor = anchor
+
+

The device anchor is the head pose — a 4×4 transform from world space to the device. The compositor requires it to be attached to the drawable before presenting; without it, the system can't reproject the frame correctly for the user's eyes.

+

The engine queries the anchor at presentation time (the future moment when the frame will appear on screen), not at "now". This is predictive — it compensates for the latency between encoding and display by predicting where the head will be.

+

Resilience strategy: ARKit can occasionally return nil for the anchor (e.g., tracking hiccup, recovery). A three-level fallback prevents dropped frames: +1. Query at predicted presentation time → use it if valid +2. Query at "now" as a retry → use it if valid +3. Fall back to lastValidDeviceAnchor — the last anchor that was valid

+

The engine never skips presenting a drawable once it has been dequeued. Even with no anchor at all, the drawable is presented (the compositor handles it gracefully); skipping would cause a more disruptive compositor error.

+
+

Step 3: GPU Encoding — executeXRSystemPass()

+

This is where the actual Metal work happens, split into three parts.

+

3a. Pre-Render Compute (runs once for both eyes)

+
performFrustumCulling(commandBuffer: commandBuffer)
+executeGaussianDepth(commandBuffer)
+executeBitonicSort(commandBuffer)
+
+

These are the same three compute passes as the macOS path: frustum cull, Gaussian depth, bitonic sort. The key difference is they run once per frame, not once per eye. The culled visibility list and sorted splat indices produced here are reused by both the left and right eye render passes.

+
+

Why only once? Running culling and sorting twice — once per eye — at 90 FPS would double the compute budget for work that produces nearly identical results (the two eyes are only ~65mm apart). One cull pass with a slightly conservative frustum covers both views.

+
+

3b. Per-Eye Render Loop

+
for (viewIndex, view) in drawable.views.enumerated() {
+    // compute view and projection matrices
+    // configure pass descriptor
+    // call renderer.renderXR(...)
+}
+
+

The drawable provides two views (left eye, right eye). For each:

+

View matrix construction: +

let cameraMatrix = simd_inverse(originFromDevice * deviceFromView)
+
+originFromDevice is the world-to-device transform from the anchor. deviceFromView is the eye offset relative to the device center (the IPD offset). Multiplying them gives world-to-eye, then inverting gives the view matrix the shaders expect.

+

Projection matrix: +

let projection = drawable.computeProjection(convention: .rightUpBack, viewIndex: viewIndex)
+
+The compositor provides the exact asymmetric projection for each eye. This accounts for the different FOV angles per eye and the physical lens geometry of the headset — it cannot be constructed manually.

+

Pass descriptor: Pre-allocated (passDescriptorLeft, passDescriptorRight) and reused every frame to avoid 180 allocations/second (2 eyes × 90 FPS). The color and depth textures are swapped in from drawable.colorTextures[viewIndex] and drawable.depthTextures[viewIndex].

+

renderer.renderXR(...) calls buildGameModeGraph() + topologicalSortGraph() + executeGraph() — the exact same render graph pipeline as macOS. The only thing that changes is: +- renderInfo.currentEye = viewIndex — tells uniform uploads which eye's matrices to use +- The base pass mode: .mixed immersion omits the base pass (camera passthrough is the background), .full immersion renders the skybox

+

3c. HZB Pyramid (built once after both eyes)

+
buildHZBDepthPyramid(commandBuffer)
+
+

After both eyes are encoded into the same command buffer, the HZB depth pyramid is built from the depth texture of the last eye rendered. In stereo, this mono pyramid is used for both per-eye occlusion tests in the next frame's frustum cull.

+

Why after both eyes, not per eye? Building HZB per eye would double the cost and produce two pyramids that next frame's single-dispatch cull can't easily consume. The right eye's depth is a reasonable approximation for the combined scene.

+

3d. Present and Commit

+
drawable.encodePresent(commandBuffer: commandBuffer)
+commandBuffer.commit()
+
+

Note encodePresent vs. macOS's commandBuffer.present(drawable). On visionOS, the present must be encoded into the command buffer as a GPU command so the compositor can precisely time when the drawable lands on screen relative to GPU work completion. It is not a CPU-side call.

+

The completion handler signals commandBufferSemaphore when the GPU finishes, freeing a slot for the next frame.

+
+

Spatial Input Bridge

+

configureSpatialEventBridge() registers a closure on layerRenderer.onSpatialEvent. Every time the compositor fires a pinch gesture or spatial tap, the closure:

+
    +
  1. Extracts the selection ray — origin and direction in world space — from the event's selectionRay field
  2. +
  3. Extracts the input device pose (hand position and orientation) if available
  4. +
  5. Maps the CompositorServices phase (.active, .ended, .cancelled) to the engine's XRSpatialInteractionPhase
  6. +
  7. Packs everything into an XRSpatialInputSnapshot and enqueues it in InputSystem
  8. +
+

The snapshot is processed on the next frame's update phase by spatialGestureRecognizer.updateSpatialInputState(), which converts raw ray/phase sequences into higher-level gesture events (tap, hold, drag) that game code can query.

+
+

The bridge is gated on isSceneReady() and !AssetLoadingGate.shared.isLoadingAny. Input events while loading are discarded to prevent game code from acting on uninitialized entities.

+
+
+

The Full XR Frame in One Picture

+
[Compositor thread] runLoop() → renderNewFrame()
+        │
+        ├─ queryNextFrame() + predictTiming()
+        ├─ frame.startUpdate()
+        │       ├─ [Main thread async] ProgressiveAssetLoader.tick()
+        │       ├─ updateSpatialInputState()   (drain gesture queue)
+        │       └─ renderer.updateXR()         (gameUpdate + handleInput)
+        ├─ frame.endUpdate()
+        ├─ wait(until: optimalInputTime)       (sleep until compositor deadline)
+        ├─ frame.startSubmission()
+        ├─ queryDrawable()                     (get per-eye textures)
+        ├─ queryDeviceAnchor() → fallback chain → drawable.deviceAnchor = anchor
+        │
+        └─ executeXRSystemPass()
+                │
+                ├─ [GPU compute] frustumCulling    (once, covers both eyes)
+                ├─ [GPU compute] gaussianDepth
+                ├─ [GPU compute] bitonicSort
+                │
+                ├─ for eye in [left, right]:
+                │       ├─ compute view matrix (originFromDevice × deviceFromView)⁻¹
+                │       ├─ compute projection  (drawable.computeProjection)
+                │       ├─ configure passDescriptor (color + depth textures)
+                │       └─ renderer.renderXR() → buildGameModeGraph()
+                │                                  topologicalSortGraph()
+                │                                  executeGraph()
+                │                                  [same DAG as macOS]
+                │
+                ├─ [GPU compute] buildHZBDepthPyramid  (once, after both eyes)
+                ├─ drawable.encodePresent(commandBuffer)
+                └─ commandBuffer.commit()
+                        │
+                        └─ [GPU→thread callback] semaphore.signal()
+        │
+        └─ frame.endSubmission()
+
+
+

Key Differences From the macOS Path

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
macOS (UpdateRenderingSystem)visionOS (executeXRSystemPass)
Frame timingMTKView drives at display rateCompositorServices dictates via optimalInputTime
Eyes12 (per-eye loop over drawable.views)
Compute passesOnce per frameOnce per frame (shared across both eyes)
View matrixCamera entity transform(originFromDevice × deviceFromView)⁻¹ from ARKit anchor
ProjectionCamera component FOVdrawable.computeProjection() — asymmetric per-eye
Present callcommandBuffer.present(drawable)drawable.encodePresent(commandBuffer:) — encoded as GPU command
HZB buildAfter the single render graphAfter both eyes, once
Base passEnvironment or gridEnvironment (full immersion) or none (mixed/passthrough)
Game update threadMain thread (MTKView delegate)Compositor thread, with main-thread dispatch for restricted APIs
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/Contributor/ContributionGuidelines/index.html b/site/Contributor/ContributionGuidelines/index.html new file mode 100644 index 00000000..cf12c907 --- /dev/null +++ b/site/Contributor/ContributionGuidelines/index.html @@ -0,0 +1,2342 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Guidelines - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + + + + +

Contributing Guidelines

+

Thank you for your interest in contributing!

+

The vision for Untold Engine is to continually shape a 3D engine that is stable, performant and developer-friendly. +As maintainer, my focus is on performance, testing, quality control, and API design.
+Contributors are encouraged to expand features, fix bugs, improve documentation, and enhance usability — always keeping the vision in mind.

+
+

Maintainer Responsibilities

+

The Untold Engine is guided by a clear vision: To be a stable, performant, and developer-friendly 3D engine that empowers creativity, removes friction, and makes game development feel effortless.

+

Guiding Principles

+

To achieve this vision, we follow these principles:

+
    +
  • The engine strives to remain stable and crash-free.
  • +
  • The codebase is backed by unit tests.
  • +
  • We profile continuously to prevent regressions (visual and performance).
  • +
  • The API must remain clear and user-friendly.
  • +
  • We always think about the developer first—removing friction so they can focus on their games.
  • +
+

As the maintainer, my primary focus is to ensure the project stays true to this vision.

+

What I Focus On

+
    +
  • Performance → keeping the renderer and systems lean, efficient, and optimized for Apple hardware.
  • +
  • Testing & Stability → maintaining a reliable codebase with proper testing practices.
  • +
  • Quality Control → reviewing PRs for clarity, readability, and adherence to coding standards.
  • +
  • API Design → ensuring that the engine’s API remains logical, intuitive, and consistent.
  • +
+

What Contributors Are Encouraged to Focus On

+
    +
  • Features → adding or improving systems, tools, and workflows.
  • +
  • Bug Fixes → addressing open issues and fixing edge cases.
  • +
  • Documentation → clarifying how things work and providing examples.
  • +
  • Editor & Usability → enhancing the UI, workflows, and overall developer experience.
  • +
+

Decision Making

+

All contributions are welcome, but acceptance will be guided by the project’s vision and the priorities above.
+PRs that align with clarity, performance, or creativity — while keeping the engine stable and simple — are more likely to be accepted.

+

These guidelines aren’t here to block you, but to make sure every contribution keeps the engine stable, clear, and useful for everyone.

+

Pull Request Guidelines

+
    +
  • +

    One feature or bug fix per PR
    + Each Pull Request should focus on a single feature, bug fix, or documentation improvement.
    + This keeps the history clean and makes it easier to track down issues later.

    +
  • +
  • +

    Commit hygiene

    +
  • +
  • Keep commits meaningful (avoid "misc changes" or "fix stuff").
  • +
  • Squash commits if needed, but do not mix unrelated features in the same commit.
  • +
  • If your PR touches multiple files, make sure they all relate to the same feature or fix.
  • +
+

✅ Example:
+- Good: “Add PhysicsSystem with gravity integration”
+- Bad: “Added PhysicsSystem + fixed rendering bug + updated docs”

+
+

Required Contributions for New System Support

+

For new systems or major features, your PR must include:

+
    +
  • Unit Tests → Validate functionality and cover edge cases.
  • +
  • How-To Guide → A step-by-step markdown guide explaining how to use the system.
  • +
+

This ensures new features are stable, documented, and accessible to all users.

+

👉 Note: For small fixes or incremental features, a How-To is not required.

+
+

How-To Guide Format

+

Your guide must follow this structure:

+
    +
  1. +

    Introduction

    +
  2. +
  3. +

    Briefly explain the feature and its purpose.

    +
  4. +
  5. +

    Describe what problem it solves or what value it adds.

    +
  6. +
  7. +

    Why Use It

    +
  8. +
  9. +

    Provide real-world examples or scenarios where the feature is useful.

    +
  10. +
  11. +

    Explain the benefits of using the feature in these contexts.

    +
  12. +
  13. +

    Step-by-Step Implementation

    +
  14. +
  15. +

    Break down the setup process into clear, actionable steps.

    +
  16. +
  17. +

    Include well-commented code snippets for each step.

    +
  18. +
  19. +

    What Happens Behind the Scenes

    +
  20. +
  21. +

    Provide technical insights into how the system works internally (if relevant).

    +
  22. +
  23. +

    Explain any significant impacts on performance or functionality.

    +
  24. +
  25. +

    Tips and Best Practices

    +
  26. +
  27. +

    Share advice for effective usage.

    +
  28. +
  29. +

    Highlight common pitfalls and how to avoid them.

    +
  30. +
  31. +

    Running the Feature

    +
  32. +
  33. +

    Explain how to test or interact with the feature after setup.

    +
  34. +
+
+

Additional Notes

+
+

Questions & Discussions

+

To keep communication clear and accessible for everyone:

+ +

This way, conversations stay organized, visible to the community, and future contributors can benefit from past discussions.

+
+

Thank you for contributing to the Untold Engine! Following these guidelines will ensure that your work aligns with the project's goals and provides value to users.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/Contributor/Formatting/index.html b/site/Contributor/Formatting/index.html new file mode 100644 index 00000000..48b80be5 --- /dev/null +++ b/site/Contributor/Formatting/index.html @@ -0,0 +1,2187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + Formatting - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+ +
+
+ + + +
+ +
+ + + + + + + + +

Formatting and Linting

+

To maintain a consistent code style across the Untold Engine repo, we use SwiftFormat. SwiftFormat is a code formatter for Swift that helps enforce Swift style conventions and keep the codebase clean. If you don't have SwiftFormat installed, see the Installing SwiftFormat section below.

+

Quick Formatting & Linting

+

Navigate to the root directory of Untold Engine and then run the commands below:

+

🔍 Lint files

+

To lint (check) all Swift files without making changes:

+
swiftformat --lint . --swiftversion 5.8 --reporter github-actions-log
+
+

Or, using the Makefile:

+
make lint
+
+

This command runs the same lint configuration as our GitHub Actions workflow and pre-commit hook, ensuring consistent results locally and in CI.

+

✅ Formatting Files

+

To format files:

+
swiftformat --swiftversion 5.8 .
+
+

Alternatively, you can use the Makefile shortcut:

+
make format
+
+

💡 Tip +If the pre-commit hook blocks your commit due to formatting issues, simply run:

+
make format
+
+

then re-stage your changes and try committing again.

+

You can bypass the hook temporarily (not recommended) with:

+
git commit --no-verify
+
+

Installing SwiftFormat

+

The simplest way to install SwiftFormat is through the command line.

+
    +
  1. Install SwiftFormat Using Homebrew: Open the terminal and run the following command:
  2. +
+

brew install swiftformat
+
+2. Verify Installation: After installation, verify that SwiftFormat is installed correctly by running:

+

swiftformat --version
+
+This should print the installed version of SwiftFormat.

+

Using SwiftFormat

+

Format a Single File

+

To format a specific Swift file:

+
    +
  1. +

    Open the terminal and navigate to your project directory.

    +
  2. +
  3. +

    Run the following command:

    +
  4. +
+

swiftformat path/to/YourFile.swift
+
+This will format YourFile.swift according to the default rules.

+

Format Multiple Files

+

To format all Swift files in your project:

+
    +
  1. +

    Navigate to your project directory in the terminal.

    +
  2. +
  3. +

    Run the following command:

    +
  4. +
+
swiftformat .
+
+

This will recursively format all Swift files in the current directory and its subdirectories.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/Contributor/versioning/index.html b/site/Contributor/versioning/index.html new file mode 100644 index 00000000..90b713a7 --- /dev/null +++ b/site/Contributor/versioning/index.html @@ -0,0 +1,1930 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Versioning - Untold Engine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + + + + + + + +

Versioning

+

To help us identity the purpose of your commits, make sure to use the following tags in your commit messages. The tags will also automatically increment the the current version when pushed to github.

+
    +
  • [Patch] - Bug fixes (eg. v1.0.0 -> v1.0.1)
  • +
  • [Feature] - For new feature that don't break compatibility (eg. v1.0.0 -> v1.1.0)
  • +
  • [API Change] - For major changes that are not API backward compatible (eg. v1.0.0 -> v2.0.0)
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/site/assets/images/favicon.png b/site/assets/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..1cf13b9f9d978896599290a74f77d5dbe7d1655c GIT binary patch literal 1870 zcmV-U2eJ5xP)Gc)JR9QMau)O=X#!i9;T z37kk-upj^(fsR36MHs_+1RCI)NNu9}lD0S{B^g8PN?Ww(5|~L#Ng*g{WsqleV}|#l zz8@ri&cTzw_h33bHI+12+kK6WN$h#n5cD8OQt`5kw6p~9H3()bUQ8OS4Q4HTQ=1Ol z_JAocz`fLbT2^{`8n~UAo=#AUOf=SOq4pYkt;XbC&f#7lb$*7=$na!mWCQ`dBQsO0 zLFBSPj*N?#u5&pf2t4XjEGH|=pPQ8xh7tpx;US5Cx_Ju;!O`ya-yF`)b%TEt5>eP1ZX~}sjjA%FJF?h7cX8=b!DZl<6%Cv z*G0uvvU+vmnpLZ2paivG-(cd*y3$hCIcsZcYOGh{$&)A6*XX&kXZd3G8m)G$Zz-LV z^GF3VAW^Mdv!)4OM8EgqRiz~*Cji;uzl2uC9^=8I84vNp;ltJ|q-*uQwGp2ma6cY7 z;`%`!9UXO@fr&Ebapfs34OmS9^u6$)bJxrucutf>`dKPKT%%*d3XlFVKunp9 zasduxjrjs>f8V=D|J=XNZp;_Zy^WgQ$9WDjgY=z@stwiEBm9u5*|34&1Na8BMjjgf3+SHcr`5~>oz1Y?SW^=K z^bTyO6>Gar#P_W2gEMwq)ot3; zREHn~U&Dp0l6YT0&k-wLwYjb?5zGK`W6S2v+K>AM(95m2C20L|3m~rN8dprPr@t)5lsk9Hu*W z?pS990s;Ez=+Rj{x7p``4>+c0G5^pYnB1^!TL=(?HLHZ+HicG{~4F1d^5Awl_2!1jICM-!9eoLhbbT^;yHcefyTAaqRcY zmuctDopPT!%k+}x%lZRKnzykr2}}XfG_ne?nRQO~?%hkzo;@RN{P6o`&mMUWBYMTe z6i8ChtjX&gXl`nvrU>jah)2iNM%JdjqoaeaU%yVn!^70x-flljp6Q5tK}5}&X8&&G zX3fpb3E(!rH=zVI_9Gjl45w@{(ITqngWFe7@9{mX;tO25Z_8 zQHEpI+FkTU#4xu>RkN>b3Tnc3UpWzPXWm#o55GKF09j^Mh~)K7{QqbO_~(@CVq! zS<8954|P8mXN2MRs86xZ&Q4EfM@JB94b=(YGuk)s&^jiSF=t3*oNK3`rD{H`yQ?d; ztE=laAUoZx5?RC8*WKOj`%LXEkgDd>&^Q4M^z`%u0rg-It=hLCVsq!Z%^6eB-OvOT zFZ28TN&cRmgU}Elrnk43)!>Z1FCPL2K$7}gwzIc48NX}#!A1BpJP?#v5wkNprhV** z?Cpalt1oH&{r!o3eSKc&ap)iz2BTn_VV`4>9M^b3;(YY}4>#ML6{~(4mH+?%07*qo IM6N<$f(jP3KmY&$ literal 0 HcmV?d00001 diff --git a/site/assets/javascripts/bundle.79ae519e.min.js b/site/assets/javascripts/bundle.79ae519e.min.js new file mode 100644 index 00000000..3df3e5e6 --- /dev/null +++ b/site/assets/javascripts/bundle.79ae519e.min.js @@ -0,0 +1,16 @@ +"use strict";(()=>{var Zi=Object.create;var _r=Object.defineProperty;var ea=Object.getOwnPropertyDescriptor;var ta=Object.getOwnPropertyNames,Bt=Object.getOwnPropertySymbols,ra=Object.getPrototypeOf,Ar=Object.prototype.hasOwnProperty,bo=Object.prototype.propertyIsEnumerable;var ho=(e,t,r)=>t in e?_r(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,P=(e,t)=>{for(var r in t||(t={}))Ar.call(t,r)&&ho(e,r,t[r]);if(Bt)for(var r of Bt(t))bo.call(t,r)&&ho(e,r,t[r]);return e};var vo=(e,t)=>{var r={};for(var o in e)Ar.call(e,o)&&t.indexOf(o)<0&&(r[o]=e[o]);if(e!=null&&Bt)for(var o of Bt(e))t.indexOf(o)<0&&bo.call(e,o)&&(r[o]=e[o]);return r};var Cr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var oa=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of ta(t))!Ar.call(e,n)&&n!==r&&_r(e,n,{get:()=>t[n],enumerable:!(o=ea(t,n))||o.enumerable});return e};var $t=(e,t,r)=>(r=e!=null?Zi(ra(e)):{},oa(t||!e||!e.__esModule?_r(r,"default",{value:e,enumerable:!0}):r,e));var go=(e,t,r)=>new Promise((o,n)=>{var i=c=>{try{a(r.next(c))}catch(p){n(p)}},s=c=>{try{a(r.throw(c))}catch(p){n(p)}},a=c=>c.done?o(c.value):Promise.resolve(c.value).then(i,s);a((r=r.apply(e,t)).next())});var xo=Cr((kr,yo)=>{(function(e,t){typeof kr=="object"&&typeof yo!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(kr,(function(){"use strict";function e(r){var o=!0,n=!1,i=null,s={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function a(k){return!!(k&&k!==document&&k.nodeName!=="HTML"&&k.nodeName!=="BODY"&&"classList"in k&&"contains"in k.classList)}function c(k){var ut=k.type,je=k.tagName;return!!(je==="INPUT"&&s[ut]&&!k.readOnly||je==="TEXTAREA"&&!k.readOnly||k.isContentEditable)}function p(k){k.classList.contains("focus-visible")||(k.classList.add("focus-visible"),k.setAttribute("data-focus-visible-added",""))}function l(k){k.hasAttribute("data-focus-visible-added")&&(k.classList.remove("focus-visible"),k.removeAttribute("data-focus-visible-added"))}function f(k){k.metaKey||k.altKey||k.ctrlKey||(a(r.activeElement)&&p(r.activeElement),o=!0)}function u(k){o=!1}function d(k){a(k.target)&&(o||c(k.target))&&p(k.target)}function v(k){a(k.target)&&(k.target.classList.contains("focus-visible")||k.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),l(k.target))}function S(k){document.visibilityState==="hidden"&&(n&&(o=!0),X())}function X(){document.addEventListener("mousemove",ee),document.addEventListener("mousedown",ee),document.addEventListener("mouseup",ee),document.addEventListener("pointermove",ee),document.addEventListener("pointerdown",ee),document.addEventListener("pointerup",ee),document.addEventListener("touchmove",ee),document.addEventListener("touchstart",ee),document.addEventListener("touchend",ee)}function re(){document.removeEventListener("mousemove",ee),document.removeEventListener("mousedown",ee),document.removeEventListener("mouseup",ee),document.removeEventListener("pointermove",ee),document.removeEventListener("pointerdown",ee),document.removeEventListener("pointerup",ee),document.removeEventListener("touchmove",ee),document.removeEventListener("touchstart",ee),document.removeEventListener("touchend",ee)}function ee(k){k.target.nodeName&&k.target.nodeName.toLowerCase()==="html"||(o=!1,re())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",u,!0),document.addEventListener("pointerdown",u,!0),document.addEventListener("touchstart",u,!0),document.addEventListener("visibilitychange",S,!0),X(),r.addEventListener("focus",d,!0),r.addEventListener("blur",v,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)}))});var ro=Cr((jy,Rn)=>{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var qa=/["'&<>]/;Rn.exports=Ka;function Ka(e){var t=""+e,r=qa.exec(t);if(!r)return t;var o,n="",i=0,s=0;for(i=r.index;i{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Nt=="object"&&typeof io=="object"?io.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Nt=="object"?Nt.ClipboardJS=r():t.ClipboardJS=r()})(Nt,function(){return(function(){var e={686:(function(o,n,i){"use strict";i.d(n,{default:function(){return Xi}});var s=i(279),a=i.n(s),c=i(370),p=i.n(c),l=i(817),f=i.n(l);function u(q){try{return document.execCommand(q)}catch(C){return!1}}var d=function(C){var _=f()(C);return u("cut"),_},v=d;function S(q){var C=document.documentElement.getAttribute("dir")==="rtl",_=document.createElement("textarea");_.style.fontSize="12pt",_.style.border="0",_.style.padding="0",_.style.margin="0",_.style.position="absolute",_.style[C?"right":"left"]="-9999px";var D=window.pageYOffset||document.documentElement.scrollTop;return _.style.top="".concat(D,"px"),_.setAttribute("readonly",""),_.value=q,_}var X=function(C,_){var D=S(C);_.container.appendChild(D);var N=f()(D);return u("copy"),D.remove(),N},re=function(C){var _=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},D="";return typeof C=="string"?D=X(C,_):C instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(C==null?void 0:C.type)?D=X(C.value,_):(D=f()(C),u("copy")),D},ee=re;function k(q){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?k=function(_){return typeof _}:k=function(_){return _&&typeof Symbol=="function"&&_.constructor===Symbol&&_!==Symbol.prototype?"symbol":typeof _},k(q)}var ut=function(){var C=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},_=C.action,D=_===void 0?"copy":_,N=C.container,G=C.target,We=C.text;if(D!=="copy"&&D!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(G!==void 0)if(G&&k(G)==="object"&&G.nodeType===1){if(D==="copy"&&G.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(D==="cut"&&(G.hasAttribute("readonly")||G.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(We)return ee(We,{container:N});if(G)return D==="cut"?v(G):ee(G,{container:N})},je=ut;function R(q){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?R=function(_){return typeof _}:R=function(_){return _&&typeof Symbol=="function"&&_.constructor===Symbol&&_!==Symbol.prototype?"symbol":typeof _},R(q)}function se(q,C){if(!(q instanceof C))throw new TypeError("Cannot call a class as a function")}function ce(q,C){for(var _=0;_0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof N.action=="function"?N.action:this.defaultAction,this.target=typeof N.target=="function"?N.target:this.defaultTarget,this.text=typeof N.text=="function"?N.text:this.defaultText,this.container=R(N.container)==="object"?N.container:document.body}},{key:"listenClick",value:function(N){var G=this;this.listener=p()(N,"click",function(We){return G.onClick(We)})}},{key:"onClick",value:function(N){var G=N.delegateTarget||N.currentTarget,We=this.action(G)||"copy",Yt=je({action:We,container:this.container,target:this.target(G),text:this.text(G)});this.emit(Yt?"success":"error",{action:We,text:Yt,trigger:G,clearSelection:function(){G&&G.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(N){return Mr("action",N)}},{key:"defaultTarget",value:function(N){var G=Mr("target",N);if(G)return document.querySelector(G)}},{key:"defaultText",value:function(N){return Mr("text",N)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(N){var G=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return ee(N,G)}},{key:"cut",value:function(N){return v(N)}},{key:"isSupported",value:function(){var N=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],G=typeof N=="string"?[N]:N,We=!!document.queryCommandSupported;return G.forEach(function(Yt){We=We&&!!document.queryCommandSupported(Yt)}),We}}]),_})(a()),Xi=Ji}),828:(function(o){var n=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function s(a,c){for(;a&&a.nodeType!==n;){if(typeof a.matches=="function"&&a.matches(c))return a;a=a.parentNode}}o.exports=s}),438:(function(o,n,i){var s=i(828);function a(l,f,u,d,v){var S=p.apply(this,arguments);return l.addEventListener(u,S,v),{destroy:function(){l.removeEventListener(u,S,v)}}}function c(l,f,u,d,v){return typeof l.addEventListener=="function"?a.apply(null,arguments):typeof u=="function"?a.bind(null,document).apply(null,arguments):(typeof l=="string"&&(l=document.querySelectorAll(l)),Array.prototype.map.call(l,function(S){return a(S,f,u,d,v)}))}function p(l,f,u,d){return function(v){v.delegateTarget=s(v.target,f),v.delegateTarget&&d.call(l,v)}}o.exports=c}),879:(function(o,n){n.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},n.nodeList=function(i){var s=Object.prototype.toString.call(i);return i!==void 0&&(s==="[object NodeList]"||s==="[object HTMLCollection]")&&"length"in i&&(i.length===0||n.node(i[0]))},n.string=function(i){return typeof i=="string"||i instanceof String},n.fn=function(i){var s=Object.prototype.toString.call(i);return s==="[object Function]"}}),370:(function(o,n,i){var s=i(879),a=i(438);function c(u,d,v){if(!u&&!d&&!v)throw new Error("Missing required arguments");if(!s.string(d))throw new TypeError("Second argument must be a String");if(!s.fn(v))throw new TypeError("Third argument must be a Function");if(s.node(u))return p(u,d,v);if(s.nodeList(u))return l(u,d,v);if(s.string(u))return f(u,d,v);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function p(u,d,v){return u.addEventListener(d,v),{destroy:function(){u.removeEventListener(d,v)}}}function l(u,d,v){return Array.prototype.forEach.call(u,function(S){S.addEventListener(d,v)}),{destroy:function(){Array.prototype.forEach.call(u,function(S){S.removeEventListener(d,v)})}}}function f(u,d,v){return a(document.body,u,d,v)}o.exports=c}),817:(function(o){function n(i){var s;if(i.nodeName==="SELECT")i.focus(),s=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var a=i.hasAttribute("readonly");a||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),a||i.removeAttribute("readonly"),s=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),p=document.createRange();p.selectNodeContents(i),c.removeAllRanges(),c.addRange(p),s=c.toString()}return s}o.exports=n}),279:(function(o){function n(){}n.prototype={on:function(i,s,a){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:s,ctx:a}),this},once:function(i,s,a){var c=this;function p(){c.off(i,p),s.apply(a,arguments)}return p._=s,this.on(i,p,a)},emit:function(i){var s=[].slice.call(arguments,1),a=((this.e||(this.e={}))[i]||[]).slice(),c=0,p=a.length;for(c;c0&&i[i.length-1])&&(p[0]===6||p[0]===2)){r=0;continue}if(p[0]===3&&(!i||p[1]>i[0]&&p[1]=e.length&&(e=void 0),{value:e&&e[o++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function K(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var o=r.call(e),n,i=[],s;try{for(;(t===void 0||t-- >0)&&!(n=o.next()).done;)i.push(n.value)}catch(a){s={error:a}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(s)throw s.error}}return i}function B(e,t,r){if(r||arguments.length===2)for(var o=0,n=t.length,i;o1||c(d,S)})},v&&(n[d]=v(n[d])))}function c(d,v){try{p(o[d](v))}catch(S){u(i[0][3],S)}}function p(d){d.value instanceof dt?Promise.resolve(d.value.v).then(l,f):u(i[0][2],d)}function l(d){c("next",d)}function f(d){c("throw",d)}function u(d,v){d(v),i.shift(),i.length&&c(i[0][0],i[0][1])}}function To(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof Oe=="function"?Oe(e):e[Symbol.iterator](),r={},o("next"),o("throw"),o("return"),r[Symbol.asyncIterator]=function(){return this},r);function o(i){r[i]=e[i]&&function(s){return new Promise(function(a,c){s=e[i](s),n(a,c,s.done,s.value)})}}function n(i,s,a,c){Promise.resolve(c).then(function(p){i({value:p,done:a})},s)}}function I(e){return typeof e=="function"}function yt(e){var t=function(o){Error.call(o),o.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var Jt=yt(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(o,n){return n+1+") "+o.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function Ze(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var qe=(function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,o,n,i;if(!this.closed){this.closed=!0;var s=this._parentage;if(s)if(this._parentage=null,Array.isArray(s))try{for(var a=Oe(s),c=a.next();!c.done;c=a.next()){var p=c.value;p.remove(this)}}catch(S){t={error:S}}finally{try{c&&!c.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}else s.remove(this);var l=this.initialTeardown;if(I(l))try{l()}catch(S){i=S instanceof Jt?S.errors:[S]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var u=Oe(f),d=u.next();!d.done;d=u.next()){var v=d.value;try{So(v)}catch(S){i=i!=null?i:[],S instanceof Jt?i=B(B([],K(i)),K(S.errors)):i.push(S)}}}catch(S){o={error:S}}finally{try{d&&!d.done&&(n=u.return)&&n.call(u)}finally{if(o)throw o.error}}}if(i)throw new Jt(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)So(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Ze(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Ze(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=(function(){var t=new e;return t.closed=!0,t})(),e})();var $r=qe.EMPTY;function Xt(e){return e instanceof qe||e&&"closed"in e&&I(e.remove)&&I(e.add)&&I(e.unsubscribe)}function So(e){I(e)?e():e.unsubscribe()}var De={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var xt={setTimeout:function(e,t){for(var r=[],o=2;o0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var o=this,n=this,i=n.hasError,s=n.isStopped,a=n.observers;return i||s?$r:(this.currentObservers=null,a.push(r),new qe(function(){o.currentObservers=null,Ze(a,r)}))},t.prototype._checkFinalizedStatuses=function(r){var o=this,n=o.hasError,i=o.thrownError,s=o.isStopped;n?r.error(i):s&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,o){return new Ho(r,o)},t})(F);var Ho=(function(e){ie(t,e);function t(r,o){var n=e.call(this)||this;return n.destination=r,n.source=o,n}return t.prototype.next=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.next)===null||n===void 0||n.call(o,r)},t.prototype.error=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.error)===null||n===void 0||n.call(o,r)},t.prototype.complete=function(){var r,o;(o=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||o===void 0||o.call(r)},t.prototype._subscribe=function(r){var o,n;return(n=(o=this.source)===null||o===void 0?void 0:o.subscribe(r))!==null&&n!==void 0?n:$r},t})(T);var jr=(function(e){ie(t,e);function t(r){var o=e.call(this)||this;return o._value=r,o}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var o=e.prototype._subscribe.call(this,r);return!o.closed&&r.next(this._value),o},t.prototype.getValue=function(){var r=this,o=r.hasError,n=r.thrownError,i=r._value;if(o)throw n;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t})(T);var Rt={now:function(){return(Rt.delegate||Date).now()},delegate:void 0};var It=(function(e){ie(t,e);function t(r,o,n){r===void 0&&(r=1/0),o===void 0&&(o=1/0),n===void 0&&(n=Rt);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=o,i._timestampProvider=n,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=o===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,o),i}return t.prototype.next=function(r){var o=this,n=o.isStopped,i=o._buffer,s=o._infiniteTimeWindow,a=o._timestampProvider,c=o._windowTime;n||(i.push(r),!s&&i.push(a.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var o=this._innerSubscribe(r),n=this,i=n._infiniteTimeWindow,s=n._buffer,a=s.slice(),c=0;c0?e.prototype.schedule.call(this,r,o):(this.delay=o,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,o){return o>0||this.closed?e.prototype.execute.call(this,r,o):this._execute(r,o)},t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!=null&&n>0||n==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.flush(this),0)},t})(St);var Ro=(function(e){ie(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t})(Ot);var Dr=new Ro(Po);var Io=(function(e){ie(t,e);function t(r,o){var n=e.call(this,r,o)||this;return n.scheduler=r,n.work=o,n}return t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!==null&&n>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.actions.push(this),r._scheduled||(r._scheduled=Tt.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,o,n){var i;if(n===void 0&&(n=0),n!=null?n>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,o,n);var s=r.actions;o!=null&&o===r._scheduled&&((i=s[s.length-1])===null||i===void 0?void 0:i.id)!==o&&(Tt.cancelAnimationFrame(o),r._scheduled=void 0)},t})(St);var Fo=(function(e){ie(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var o;r?o=r.id:(o=this._scheduled,this._scheduled=void 0);var n=this.actions,i;r=r||n.shift();do if(i=r.execute(r.state,r.delay))break;while((r=n[0])&&r.id===o&&n.shift());if(this._active=!1,i){for(;(r=n[0])&&r.id===o&&n.shift();)r.unsubscribe();throw i}},t})(Ot);var ye=new Fo(Io);var y=new F(function(e){return e.complete()});function tr(e){return e&&I(e.schedule)}function Vr(e){return e[e.length-1]}function pt(e){return I(Vr(e))?e.pop():void 0}function Fe(e){return tr(Vr(e))?e.pop():void 0}function rr(e,t){return typeof Vr(e)=="number"?e.pop():t}var Lt=(function(e){return e&&typeof e.length=="number"&&typeof e!="function"});function or(e){return I(e==null?void 0:e.then)}function nr(e){return I(e[wt])}function ir(e){return Symbol.asyncIterator&&I(e==null?void 0:e[Symbol.asyncIterator])}function ar(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function fa(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var sr=fa();function cr(e){return I(e==null?void 0:e[sr])}function pr(e){return wo(this,arguments,function(){var r,o,n,i;return Gt(this,function(s){switch(s.label){case 0:r=e.getReader(),s.label=1;case 1:s.trys.push([1,,9,10]),s.label=2;case 2:return[4,dt(r.read())];case 3:return o=s.sent(),n=o.value,i=o.done,i?[4,dt(void 0)]:[3,5];case 4:return[2,s.sent()];case 5:return[4,dt(n)];case 6:return[4,s.sent()];case 7:return s.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function lr(e){return I(e==null?void 0:e.getReader)}function U(e){if(e instanceof F)return e;if(e!=null){if(nr(e))return ua(e);if(Lt(e))return da(e);if(or(e))return ha(e);if(ir(e))return jo(e);if(cr(e))return ba(e);if(lr(e))return va(e)}throw ar(e)}function ua(e){return new F(function(t){var r=e[wt]();if(I(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function da(e){return new F(function(t){for(var r=0;r=2;return function(o){return o.pipe(e?g(function(n,i){return e(n,i,o)}):be,Ee(1),r?Qe(t):tn(function(){return new fr}))}}function Yr(e){return e<=0?function(){return y}:E(function(t,r){var o=[];t.subscribe(w(r,function(n){o.push(n),e=2,!0))}function le(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new T}:t,o=e.resetOnError,n=o===void 0?!0:o,i=e.resetOnComplete,s=i===void 0?!0:i,a=e.resetOnRefCountZero,c=a===void 0?!0:a;return function(p){var l,f,u,d=0,v=!1,S=!1,X=function(){f==null||f.unsubscribe(),f=void 0},re=function(){X(),l=u=void 0,v=S=!1},ee=function(){var k=l;re(),k==null||k.unsubscribe()};return E(function(k,ut){d++,!S&&!v&&X();var je=u=u!=null?u:r();ut.add(function(){d--,d===0&&!S&&!v&&(f=Br(ee,c))}),je.subscribe(ut),!l&&d>0&&(l=new bt({next:function(R){return je.next(R)},error:function(R){S=!0,X(),f=Br(re,n,R),je.error(R)},complete:function(){v=!0,X(),f=Br(re,s),je.complete()}}),U(k).subscribe(l))})(p)}}function Br(e,t){for(var r=[],o=2;oe.next(document)),e}function M(e,t=document){return Array.from(t.querySelectorAll(e))}function j(e,t=document){let r=ue(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function ue(e,t=document){return t.querySelector(e)||void 0}function Ne(){var e,t,r,o;return(o=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?o:void 0}var Ra=L(h(document.body,"focusin"),h(document.body,"focusout")).pipe(Ae(1),Q(void 0),m(()=>Ne()||document.body),Z(1));function Ye(e){return Ra.pipe(m(t=>e.contains(t)),Y())}function it(e,t){return H(()=>L(h(e,"mouseenter").pipe(m(()=>!0)),h(e,"mouseleave").pipe(m(()=>!1))).pipe(t?jt(r=>He(+!r*t)):be,Q(e.matches(":hover"))))}function sn(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)sn(e,r)}function x(e,t,...r){let o=document.createElement(e);if(t)for(let n of Object.keys(t))typeof t[n]!="undefined"&&(typeof t[n]!="boolean"?o.setAttribute(n,t[n]):o.setAttribute(n,""));for(let n of r)sn(o,n);return o}function br(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function _t(e){let t=x("script",{src:e});return H(()=>(document.head.appendChild(t),L(h(t,"load"),h(t,"error").pipe(b(()=>Nr(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(m(()=>{}),A(()=>document.head.removeChild(t)),Ee(1))))}var cn=new T,Ia=H(()=>typeof ResizeObserver=="undefined"?_t("https://unpkg.com/resize-observer-polyfill"):$(void 0)).pipe(m(()=>new ResizeObserver(e=>e.forEach(t=>cn.next(t)))),b(e=>L(tt,$(e)).pipe(A(()=>e.disconnect()))),Z(1));function de(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Le(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return Ia.pipe(O(r=>r.observe(t)),b(r=>cn.pipe(g(o=>o.target===t),A(()=>r.unobserve(t)))),m(()=>de(e)),Q(de(e)))}function At(e){return{width:e.scrollWidth,height:e.scrollHeight}}function vr(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function pn(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function Be(e){return{x:e.offsetLeft,y:e.offsetTop}}function ln(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function mn(e){return L(h(window,"load"),h(window,"resize")).pipe($e(0,ye),m(()=>Be(e)),Q(Be(e)))}function gr(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ge(e){return L(h(e,"scroll"),h(window,"scroll"),h(window,"resize")).pipe($e(0,ye),m(()=>gr(e)),Q(gr(e)))}var fn=new T,Fa=H(()=>$(new IntersectionObserver(e=>{for(let t of e)fn.next(t)},{threshold:0}))).pipe(b(e=>L(tt,$(e)).pipe(A(()=>e.disconnect()))),Z(1));function mt(e){return Fa.pipe(O(t=>t.observe(e)),b(t=>fn.pipe(g(({target:r})=>r===e),A(()=>t.unobserve(e)),m(({isIntersecting:r})=>r))))}function un(e,t=16){return Ge(e).pipe(m(({y:r})=>{let o=de(e),n=At(e);return r>=n.height-o.height-t}),Y())}var yr={drawer:j("[data-md-toggle=drawer]"),search:j("[data-md-toggle=search]")};function dn(e){return yr[e].checked}function at(e,t){yr[e].checked!==t&&yr[e].click()}function Je(e){let t=yr[e];return h(t,"change").pipe(m(()=>t.checked),Q(t.checked))}function ja(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ua(){return L(h(window,"compositionstart").pipe(m(()=>!0)),h(window,"compositionend").pipe(m(()=>!1))).pipe(Q(!1))}function hn(){let e=h(window,"keydown").pipe(g(t=>!(t.metaKey||t.ctrlKey)),m(t=>({mode:dn("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),g(({mode:t,type:r})=>{if(t==="global"){let o=Ne();if(typeof o!="undefined")return!ja(o,r)}return!0}),le());return Ua().pipe(b(t=>t?y:e))}function we(){return new URL(location.href)}function st(e,t=!1){if(V("navigation.instant")&&!t){let r=x("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function bn(){return new T}function vn(){return location.hash.slice(1)}function gn(e){let t=x("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Zr(e){return L(h(window,"hashchange"),e).pipe(m(vn),Q(vn()),g(t=>t.length>0),Z(1))}function yn(e){return Zr(e).pipe(m(t=>ue(`[id="${t}"]`)),g(t=>typeof t!="undefined"))}function Wt(e){let t=matchMedia(e);return ur(r=>t.addListener(()=>r(t.matches))).pipe(Q(t.matches))}function xn(){let e=matchMedia("print");return L(h(window,"beforeprint").pipe(m(()=>!0)),h(window,"afterprint").pipe(m(()=>!1))).pipe(Q(e.matches))}function eo(e,t){return e.pipe(b(r=>r?t():y))}function to(e,t){return new F(r=>{let o=new XMLHttpRequest;return o.open("GET",`${e}`),o.responseType="blob",o.addEventListener("load",()=>{o.status>=200&&o.status<300?(r.next(o.response),r.complete()):r.error(new Error(o.statusText))}),o.addEventListener("error",()=>{r.error(new Error("Network error"))}),o.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(o.addEventListener("progress",n=>{var i;if(n.lengthComputable)t.progress$.next(n.loaded/n.total*100);else{let s=(i=o.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(n.loaded/+s*100)}}),t.progress$.next(5)),o.send(),()=>o.abort()})}function ze(e,t){return to(e,t).pipe(b(r=>r.text()),m(r=>JSON.parse(r)),Z(1))}function xr(e,t){let r=new DOMParser;return to(e,t).pipe(b(o=>o.text()),m(o=>r.parseFromString(o,"text/html")),Z(1))}function En(e,t){let r=new DOMParser;return to(e,t).pipe(b(o=>o.text()),m(o=>r.parseFromString(o,"text/xml")),Z(1))}function wn(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function Tn(){return L(h(window,"scroll",{passive:!0}),h(window,"resize",{passive:!0})).pipe(m(wn),Q(wn()))}function Sn(){return{width:innerWidth,height:innerHeight}}function On(){return h(window,"resize",{passive:!0}).pipe(m(Sn),Q(Sn()))}function Ln(){return z([Tn(),On()]).pipe(m(([e,t])=>({offset:e,size:t})),Z(1))}function Er(e,{viewport$:t,header$:r}){let o=t.pipe(ne("size")),n=z([o,r]).pipe(m(()=>Be(e)));return z([r,t,n]).pipe(m(([{height:i},{offset:s,size:a},{x:c,y:p}])=>({offset:{x:s.x-c,y:s.y-p+i},size:a})))}function Wa(e){return h(e,"message",t=>t.data)}function Da(e){let t=new T;return t.subscribe(r=>e.postMessage(r)),t}function Mn(e,t=new Worker(e)){let r=Wa(t),o=Da(t),n=new T;n.subscribe(o);let i=o.pipe(oe(),ae(!0));return n.pipe(oe(),Ve(r.pipe(W(i))),le())}var Va=j("#__config"),Ct=JSON.parse(Va.textContent);Ct.base=`${new URL(Ct.base,we())}`;function Te(){return Ct}function V(e){return Ct.features.includes(e)}function Me(e,t){return typeof t!="undefined"?Ct.translations[e].replace("#",t.toString()):Ct.translations[e]}function Ce(e,t=document){return j(`[data-md-component=${e}]`,t)}function me(e,t=document){return M(`[data-md-component=${e}]`,t)}function Na(e){let t=j(".md-typeset > :first-child",e);return h(t,"click",{once:!0}).pipe(m(()=>j(".md-typeset",e)),m(r=>({hash:__md_hash(r.innerHTML)})))}function _n(e){if(!V("announce.dismiss")||!e.childElementCount)return y;if(!e.hidden){let t=j(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return H(()=>{let t=new T;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),Na(e).pipe(O(r=>t.next(r)),A(()=>t.complete()),m(r=>P({ref:e},r)))})}function za(e,{target$:t}){return t.pipe(m(r=>({hidden:r!==e})))}function An(e,t){let r=new T;return r.subscribe(({hidden:o})=>{e.hidden=o}),za(e,t).pipe(O(o=>r.next(o)),A(()=>r.complete()),m(o=>P({ref:e},o)))}function Dt(e,t){return t==="inline"?x("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"})):x("div",{class:"md-tooltip",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"}))}function wr(...e){return x("div",{class:"md-tooltip2",role:"dialog"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function Cn(...e){return x("div",{class:"md-tooltip2",role:"tooltip"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function kn(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return x("aside",{class:"md-annotation",tabIndex:0},Dt(t),x("a",{href:r,class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}else return x("aside",{class:"md-annotation",tabIndex:0},Dt(t),x("span",{class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}function Hn(e){return x("button",{class:"md-code__button",title:Me("clipboard.copy"),"data-clipboard-target":`#${e} > code`,"data-md-type":"copy"})}function $n(){return x("button",{class:"md-code__button",title:"Toggle line selection","data-md-type":"select"})}function Pn(){return x("nav",{class:"md-code__nav"})}var In=$t(ro());function oo(e,t){let r=t&2,o=t&1,n=Object.keys(e.terms).filter(c=>!e.terms[c]).reduce((c,p)=>[...c,x("del",null,(0,In.default)(p))," "],[]).slice(0,-1),i=Te(),s=new URL(e.location,i.base);V("search.highlight")&&s.searchParams.set("h",Object.entries(e.terms).filter(([,c])=>c).reduce((c,[p])=>`${c} ${p}`.trim(),""));let{tags:a}=Te();return x("a",{href:`${s}`,class:"md-search-result__link",tabIndex:-1},x("article",{class:"md-search-result__article md-typeset","data-md-score":e.score.toFixed(2)},r>0&&x("div",{class:"md-search-result__icon md-icon"}),r>0&&x("h1",null,e.title),r<=0&&x("h2",null,e.title),o>0&&e.text.length>0&&e.text,e.tags&&x("nav",{class:"md-tags"},e.tags.map(c=>{let p=a?c in a?`md-tag-icon md-tag--${a[c]}`:"md-tag-icon":"";return x("span",{class:`md-tag ${p}`},c)})),o>0&&n.length>0&&x("p",{class:"md-search-result__terms"},Me("search.result.term.missing"),": ",...n)))}function Fn(e){let t=e[0].score,r=[...e],o=Te(),n=r.findIndex(l=>!`${new URL(l.location,o.base)}`.includes("#")),[i]=r.splice(n,1),s=r.findIndex(l=>l.scoreoo(l,1)),...c.length?[x("details",{class:"md-search-result__more"},x("summary",{tabIndex:-1},x("div",null,c.length>0&&c.length===1?Me("search.result.more.one"):Me("search.result.more.other",c.length))),...c.map(l=>oo(l,1)))]:[]];return x("li",{class:"md-search-result__item"},p)}function jn(e){return x("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>x("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?br(r):r)))}function no(e){let t=`tabbed-control tabbed-control--${e}`;return x("div",{class:t,hidden:!0},x("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function Un(e){return x("div",{class:"md-typeset__scrollwrap"},x("div",{class:"md-typeset__table"},e))}function Qa(e){var o;let t=Te(),r=new URL(`../${e.version}/`,t.base);return x("li",{class:"md-version__item"},x("a",{href:`${r}`,class:"md-version__link"},e.title,((o=t.version)==null?void 0:o.alias)&&e.aliases.length>0&&x("span",{class:"md-version__alias"},e.aliases[0])))}function Wn(e,t){var o;let r=Te();return e=e.filter(n=>{var i;return!((i=n.properties)!=null&&i.hidden)}),x("div",{class:"md-version"},x("button",{class:"md-version__current","aria-label":Me("select.version")},t.title,((o=r.version)==null?void 0:o.alias)&&t.aliases.length>0&&x("span",{class:"md-version__alias"},t.aliases[0])),x("ul",{class:"md-version__list"},e.map(Qa)))}var Ya=0;function Ba(e,t=250){let r=z([Ye(e),it(e,t)]).pipe(m(([n,i])=>n||i),Y()),o=H(()=>pn(e)).pipe(J(Ge),gt(1),Pe(r),m(()=>ln(e)));return r.pipe(Re(n=>n),b(()=>z([r,o])),m(([n,i])=>({active:n,offset:i})),le())}function Vt(e,t,r=250){let{content$:o,viewport$:n}=t,i=`__tooltip2_${Ya++}`;return H(()=>{let s=new T,a=new jr(!1);s.pipe(oe(),ae(!1)).subscribe(a);let c=a.pipe(jt(l=>He(+!l*250,Dr)),Y(),b(l=>l?o:y),O(l=>l.id=i),le());z([s.pipe(m(({active:l})=>l)),c.pipe(b(l=>it(l,250)),Q(!1))]).pipe(m(l=>l.some(f=>f))).subscribe(a);let p=a.pipe(g(l=>l),te(c,n),m(([l,f,{size:u}])=>{let d=e.getBoundingClientRect(),v=d.width/2;if(f.role==="tooltip")return{x:v,y:8+d.height};if(d.y>=u.height/2){let{height:S}=de(f);return{x:v,y:-16-S}}else return{x:v,y:16+d.height}}));return z([c,s,p]).subscribe(([l,{offset:f},u])=>{l.style.setProperty("--md-tooltip-host-x",`${f.x}px`),l.style.setProperty("--md-tooltip-host-y",`${f.y}px`),l.style.setProperty("--md-tooltip-x",`${u.x}px`),l.style.setProperty("--md-tooltip-y",`${u.y}px`),l.classList.toggle("md-tooltip2--top",u.y<0),l.classList.toggle("md-tooltip2--bottom",u.y>=0)}),a.pipe(g(l=>l),te(c,(l,f)=>f),g(l=>l.role==="tooltip")).subscribe(l=>{let f=de(j(":scope > *",l));l.style.setProperty("--md-tooltip-width",`${f.width}px`),l.style.setProperty("--md-tooltip-tail","0px")}),a.pipe(Y(),xe(ye),te(c)).subscribe(([l,f])=>{f.classList.toggle("md-tooltip2--active",l)}),z([a.pipe(g(l=>l)),c]).subscribe(([l,f])=>{f.role==="dialog"?(e.setAttribute("aria-controls",i),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",i)}),a.pipe(g(l=>!l)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),Ba(e,r).pipe(O(l=>s.next(l)),A(()=>s.complete()),m(l=>P({ref:e},l)))})}function Xe(e,{viewport$:t},r=document.body){return Vt(e,{content$:new F(o=>{let n=e.title,i=Cn(n);return o.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",n)}}),viewport$:t},0)}function Ga(e,t){let r=H(()=>z([mn(e),Ge(t)])).pipe(m(([{x:o,y:n},i])=>{let{width:s,height:a}=de(e);return{x:o-i.x+s/2,y:n-i.y+a/2}}));return Ye(e).pipe(b(o=>r.pipe(m(n=>({active:o,offset:n})),Ee(+!o||1/0))))}function Dn(e,t,{target$:r}){let[o,n]=Array.from(e.children);return H(()=>{let i=new T,s=i.pipe(oe(),ae(!0));return i.subscribe({next({offset:a}){e.style.setProperty("--md-tooltip-x",`${a.x}px`),e.style.setProperty("--md-tooltip-y",`${a.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),mt(e).pipe(W(s)).subscribe(a=>{e.toggleAttribute("data-md-visible",a)}),L(i.pipe(g(({active:a})=>a)),i.pipe(Ae(250),g(({active:a})=>!a))).subscribe({next({active:a}){a?e.prepend(o):o.remove()},complete(){e.prepend(o)}}),i.pipe($e(16,ye)).subscribe(({active:a})=>{o.classList.toggle("md-tooltip--active",a)}),i.pipe(gt(125,ye),g(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:a})=>a)).subscribe({next(a){a?e.style.setProperty("--md-tooltip-0",`${-a}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),h(n,"click").pipe(W(s),g(a=>!(a.metaKey||a.ctrlKey))).subscribe(a=>{a.stopPropagation(),a.preventDefault()}),h(n,"mousedown").pipe(W(s),te(i)).subscribe(([a,{active:c}])=>{var p;if(a.button!==0||a.metaKey||a.ctrlKey)a.preventDefault();else if(c){a.preventDefault();let l=e.parentElement.closest(".md-annotation");l instanceof HTMLElement?l.focus():(p=Ne())==null||p.blur()}}),r.pipe(W(s),g(a=>a===o),nt(125)).subscribe(()=>e.focus()),Ga(e,t).pipe(O(a=>i.next(a)),A(()=>i.complete()),m(a=>P({ref:e},a)))})}function Ja(e){let t=Te();if(e.tagName!=="CODE")return[e];let r=[".c",".c1",".cm"];if(t.annotate&&typeof t.annotate=="object"){let o=e.closest("[class|=language]");if(o)for(let n of Array.from(o.classList)){if(!n.startsWith("language-"))continue;let[,i]=n.split("-");i in t.annotate&&r.push(...t.annotate[i])}}return M(r.join(", "),e)}function Xa(e){let t=[];for(let r of Ja(e)){let o=[],n=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=n.nextNode();i;i=n.nextNode())o.push(i);for(let i of o){let s;for(;s=/(\(\d+\))(!)?/.exec(i.textContent);){let[,a,c]=s;if(typeof c=="undefined"){let p=i.splitText(s.index);i=p.splitText(a.length),t.push(p)}else{i.textContent=a,t.push(i);break}}}}return t}function Vn(e,t){t.append(...Array.from(e.childNodes))}function Tr(e,t,{target$:r,print$:o}){let n=t.closest("[id]"),i=n==null?void 0:n.id,s=new Map;for(let a of Xa(t)){let[,c]=a.textContent.match(/\((\d+)\)/);ue(`:scope > li:nth-child(${c})`,e)&&(s.set(c,kn(c,i)),a.replaceWith(s.get(c)))}return s.size===0?y:H(()=>{let a=new T,c=a.pipe(oe(),ae(!0)),p=[];for(let[l,f]of s)p.push([j(".md-typeset",f),j(`:scope > li:nth-child(${l})`,e)]);return o.pipe(W(c)).subscribe(l=>{e.hidden=!l,e.classList.toggle("md-annotation-list",l);for(let[f,u]of p)l?Vn(f,u):Vn(u,f)}),L(...[...s].map(([,l])=>Dn(l,t,{target$:r}))).pipe(A(()=>a.complete()),le())})}function Nn(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Nn(t)}}function zn(e,t){return H(()=>{let r=Nn(e);return typeof r!="undefined"?Tr(r,e,t):y})}var Kn=$t(ao());var Za=0,qn=L(h(window,"keydown").pipe(m(()=>!0)),L(h(window,"keyup"),h(window,"contextmenu")).pipe(m(()=>!1))).pipe(Q(!1),Z(1));function Qn(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Qn(t)}}function es(e){return Le(e).pipe(m(({width:t})=>({scrollable:At(e).width>t})),ne("scrollable"))}function Yn(e,t){let{matches:r}=matchMedia("(hover)"),o=H(()=>{let n=new T,i=n.pipe(Yr(1));n.subscribe(({scrollable:d})=>{d&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let s=[],a=e.closest("pre"),c=a.closest("[id]"),p=c?c.id:Za++;a.id=`__code_${p}`;let l=[],f=e.closest(".highlight");if(f instanceof HTMLElement){let d=Qn(f);if(typeof d!="undefined"&&(f.classList.contains("annotate")||V("content.code.annotate"))){let v=Tr(d,e,t);l.push(Le(f).pipe(W(i),m(({width:S,height:X})=>S&&X),Y(),b(S=>S?v:y)))}}let u=M(":scope > span[id]",e);if(u.length&&(e.classList.add("md-code__content"),e.closest(".select")||V("content.code.select")&&!e.closest(".no-select"))){let d=+u[0].id.split("-").pop(),v=$n();s.push(v),V("content.tooltips")&&l.push(Xe(v,{viewport$}));let S=h(v,"click").pipe(Ut(R=>!R,!1),O(()=>v.blur()),le());S.subscribe(R=>{v.classList.toggle("md-code__button--active",R)});let X=fe(u).pipe(J(R=>it(R).pipe(m(se=>[R,se]))));S.pipe(b(R=>R?X:y)).subscribe(([R,se])=>{let ce=ue(".hll.select",R);if(ce&&!se)ce.replaceWith(...Array.from(ce.childNodes));else if(!ce&&se){let he=document.createElement("span");he.className="hll select",he.append(...Array.from(R.childNodes).slice(1)),R.append(he)}});let re=fe(u).pipe(J(R=>h(R,"mousedown").pipe(O(se=>se.preventDefault()),m(()=>R)))),ee=S.pipe(b(R=>R?re:y),te(qn),m(([R,se])=>{var he;let ce=u.indexOf(R)+d;if(se===!1)return[ce,ce];{let Se=M(".hll",e).map(Ue=>u.indexOf(Ue.parentElement)+d);return(he=window.getSelection())==null||he.removeAllRanges(),[Math.min(ce,...Se),Math.max(ce,...Se)]}})),k=Zr(y).pipe(g(R=>R.startsWith(`__codelineno-${p}-`)));k.subscribe(R=>{let[,,se]=R.split("-"),ce=se.split(":").map(Se=>+Se-d+1);ce.length===1&&ce.push(ce[0]);for(let Se of M(".hll:not(.select)",e))Se.replaceWith(...Array.from(Se.childNodes));let he=u.slice(ce[0]-1,ce[1]);for(let Se of he){let Ue=document.createElement("span");Ue.className="hll",Ue.append(...Array.from(Se.childNodes).slice(1)),Se.append(Ue)}}),k.pipe(Ee(1),xe(pe)).subscribe(R=>{if(R.includes(":")){let se=document.getElementById(R.split(":")[0]);se&&setTimeout(()=>{let ce=se,he=-64;for(;ce!==document.body;)he+=ce.offsetTop,ce=ce.offsetParent;window.scrollTo({top:he})},1)}});let je=fe(M('a[href^="#__codelineno"]',f)).pipe(J(R=>h(R,"click").pipe(O(se=>se.preventDefault()),m(()=>R)))).pipe(W(i),te(qn),m(([R,se])=>{let he=+j(`[id="${R.hash.slice(1)}"]`).parentElement.id.split("-").pop();if(se===!1)return[he,he];{let Se=M(".hll",e).map(Ue=>+Ue.parentElement.id.split("-").pop());return[Math.min(he,...Se),Math.max(he,...Se)]}}));L(ee,je).subscribe(R=>{let se=`#__codelineno-${p}-`;R[0]===R[1]?se+=R[0]:se+=`${R[0]}:${R[1]}`,history.replaceState({},"",se),window.dispatchEvent(new HashChangeEvent("hashchange",{newURL:window.location.origin+window.location.pathname+se,oldURL:window.location.href}))})}if(Kn.default.isSupported()&&(e.closest(".copy")||V("content.code.copy")&&!e.closest(".no-copy"))){let d=Hn(a.id);s.push(d),V("content.tooltips")&&l.push(Xe(d,{viewport$}))}if(s.length){let d=Pn();d.append(...s),a.insertBefore(d,e)}return es(e).pipe(O(d=>n.next(d)),A(()=>n.complete()),m(d=>P({ref:e},d)),Ve(L(...l).pipe(W(i))))});return V("content.lazy")?mt(e).pipe(g(n=>n),Ee(1),b(()=>o)):o}function ts(e,{target$:t,print$:r}){let o=!0;return L(t.pipe(m(n=>n.closest("details:not([open])")),g(n=>e===n),m(()=>({action:"open",reveal:!0}))),r.pipe(g(n=>n||!o),O(()=>o=e.open),m(n=>({action:n?"open":"close"}))))}function Bn(e,t){return H(()=>{let r=new T;return r.subscribe(({action:o,reveal:n})=>{e.toggleAttribute("open",o==="open"),n&&e.scrollIntoView()}),ts(e,t).pipe(O(o=>r.next(o)),A(()=>r.complete()),m(o=>P({ref:e},o)))})}var Gn=0;function rs(e){let t=document.createElement("h3");t.innerHTML=e.innerHTML;let r=[t],o=e.nextElementSibling;for(;o&&!(o instanceof HTMLHeadingElement);)r.push(o),o=o.nextElementSibling;return r}function os(e,t){for(let r of M("[href], [src]",e))for(let o of["href","src"]){let n=r.getAttribute(o);if(n&&!/^(?:[a-z]+:)?\/\//i.test(n)){r[o]=new URL(r.getAttribute(o),t).toString();break}}for(let r of M("[name^=__], [for]",e))for(let o of["id","for","name"]){let n=r.getAttribute(o);n&&r.setAttribute(o,`${n}$preview_${Gn}`)}return Gn++,$(e)}function Jn(e,t){let{sitemap$:r}=t;if(!(e instanceof HTMLAnchorElement))return y;if(!(V("navigation.instant.preview")||e.hasAttribute("data-preview")))return y;e.removeAttribute("title");let o=z([Ye(e),it(e)]).pipe(m(([i,s])=>i||s),Y(),g(i=>i));return rt([r,o]).pipe(b(([i])=>{let s=new URL(e.href);return s.search=s.hash="",i.has(`${s}`)?$(s):y}),b(i=>xr(i).pipe(b(s=>os(s,i)))),b(i=>{let s=e.hash?`article [id="${e.hash.slice(1)}"]`:"article h1",a=ue(s,i);return typeof a=="undefined"?y:$(rs(a))})).pipe(b(i=>{let s=new F(a=>{let c=wr(...i);return a.next(c),document.body.append(c),()=>c.remove()});return Vt(e,P({content$:s},t))}))}var Xn=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color)}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs marker.marker.composition.class path,defs marker.marker.dependency.class path,defs marker.marker.extension.class path{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs marker.marker.aggregation.class path{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}[id^=entity] path,[id^=entity] rect{fill:var(--md-default-bg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs .marker.oneOrMore.er *,defs .marker.onlyOne.er *,defs .marker.zeroOrMore.er *,defs .marker.zeroOrOne.er *{stroke:var(--md-mermaid-edge-color)!important}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var so,is=0;function as(){return typeof mermaid=="undefined"||mermaid instanceof Element?_t("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):$(void 0)}function Zn(e){return e.classList.remove("mermaid"),so||(so=as().pipe(O(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Xn,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),m(()=>{}),Z(1))),so.subscribe(()=>go(null,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${is++}`,r=x("div",{class:"mermaid"}),o=e.textContent,{svg:n,fn:i}=yield mermaid.render(t,o),s=r.attachShadow({mode:"closed"});s.innerHTML=n,e.replaceWith(r),i==null||i(s)})),so.pipe(m(()=>({ref:e})))}var ei=x("table");function ti(e){return e.replaceWith(ei),ei.replaceWith(Un(e)),$({ref:e})}function ss(e){let t=e.find(r=>r.checked)||e[0];return L(...e.map(r=>h(r,"change").pipe(m(()=>j(`label[for="${r.id}"]`))))).pipe(Q(j(`label[for="${t.id}"]`)),m(r=>({active:r})))}function ri(e,{viewport$:t,target$:r}){let o=j(".tabbed-labels",e),n=M(":scope > input",e),i=no("prev");e.append(i);let s=no("next");return e.append(s),H(()=>{let a=new T,c=a.pipe(oe(),ae(!0));z([a,Le(e),mt(e)]).pipe(W(c),$e(1,ye)).subscribe({next([{active:p},l]){let f=Be(p),{width:u}=de(p);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${u}px`);let d=gr(o);(f.xd.x+l.width)&&o.scrollTo({left:Math.max(0,f.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),z([Ge(o),Le(o)]).pipe(W(c)).subscribe(([p,l])=>{let f=At(o);i.hidden=p.x<16,s.hidden=p.x>f.width-l.width-16}),L(h(i,"click").pipe(m(()=>-1)),h(s,"click").pipe(m(()=>1))).pipe(W(c)).subscribe(p=>{let{width:l}=de(o);o.scrollBy({left:l*p,behavior:"smooth"})}),r.pipe(W(c),g(p=>n.includes(p))).subscribe(p=>p.click()),o.classList.add("tabbed-labels--linked");for(let p of n){let l=j(`label[for="${p.id}"]`);l.replaceChildren(x("a",{href:`#${l.htmlFor}`,tabIndex:-1},...Array.from(l.childNodes))),h(l.firstElementChild,"click").pipe(W(c),g(f=>!(f.metaKey||f.ctrlKey)),O(f=>{f.preventDefault(),f.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${l.htmlFor}`),l.click()})}return V("content.tabs.link")&&a.pipe(Ie(1),te(t)).subscribe(([{active:p},{offset:l}])=>{let f=p.innerText.trim();if(p.hasAttribute("data-md-switching"))p.removeAttribute("data-md-switching");else{let u=e.offsetTop-l.y;for(let v of M("[data-tabs]"))for(let S of M(":scope > input",v)){let X=j(`label[for="${S.id}"]`);if(X!==p&&X.innerText.trim()===f){X.setAttribute("data-md-switching",""),S.click();break}}window.scrollTo({top:e.offsetTop-u});let d=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...d])])}}),a.pipe(W(c)).subscribe(()=>{for(let p of M("audio, video",e))p.offsetWidth&&p.autoplay?p.play().catch(()=>{}):p.pause()}),ss(n).pipe(O(p=>a.next(p)),A(()=>a.complete()),m(p=>P({ref:e},p)))}).pipe(et(pe))}function oi(e,t){let{viewport$:r,target$:o,print$:n}=t;return L(...M(".annotate:not(.highlight)",e).map(i=>zn(i,{target$:o,print$:n})),...M("pre:not(.mermaid) > code",e).map(i=>Yn(i,{target$:o,print$:n})),...M("a",e).map(i=>Jn(i,t)),...M("pre.mermaid",e).map(i=>Zn(i)),...M("table:not([class])",e).map(i=>ti(i)),...M("details",e).map(i=>Bn(i,{target$:o,print$:n})),...M("[data-tabs]",e).map(i=>ri(i,{viewport$:r,target$:o})),...M("[title]:not([data-preview])",e).filter(()=>V("content.tooltips")).map(i=>Xe(i,{viewport$:r})),...M(".footnote-ref",e).filter(()=>V("content.footnote.tooltips")).map(i=>Vt(i,{content$:new F(s=>{let a=new URL(i.href).hash.slice(1),c=Array.from(document.getElementById(a).cloneNode(!0).children),p=wr(...c);return s.next(p),document.body.append(p),()=>p.remove()}),viewport$:r})))}function cs(e,{alert$:t}){return t.pipe(b(r=>L($(!0),$(!1).pipe(nt(2e3))).pipe(m(o=>({message:r,active:o})))))}function ni(e,t){let r=j(".md-typeset",e);return H(()=>{let o=new T;return o.subscribe(({message:n,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=n}),cs(e,t).pipe(O(n=>o.next(n)),A(()=>o.complete()),m(n=>P({ref:e},n)))})}var ps=0;function ls(e,t){document.body.append(e);let{width:r}=de(e);e.style.setProperty("--md-tooltip-width",`${r}px`),e.remove();let o=vr(t),n=typeof o!="undefined"?Ge(o):$({x:0,y:0}),i=L(Ye(t),it(t)).pipe(Y());return z([i,n]).pipe(m(([s,a])=>{let{x:c,y:p}=Be(t),l=de(t),f=t.closest("table");return f&&t.parentElement&&(c+=f.offsetLeft+t.parentElement.offsetLeft,p+=f.offsetTop+t.parentElement.offsetTop),{active:s,offset:{x:c-a.x+l.width/2-r/2,y:p-a.y+l.height+8}}}))}function ii(e){let t=e.title;if(!t.length)return y;let r=`__tooltip_${ps++}`,o=Dt(r,"inline"),n=j(".md-typeset",o);return n.innerHTML=t,H(()=>{let i=new T;return i.subscribe({next({offset:s}){o.style.setProperty("--md-tooltip-x",`${s.x}px`),o.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){o.style.removeProperty("--md-tooltip-x"),o.style.removeProperty("--md-tooltip-y")}}),L(i.pipe(g(({active:s})=>s)),i.pipe(Ae(250),g(({active:s})=>!s))).subscribe({next({active:s}){s?(e.insertAdjacentElement("afterend",o),e.setAttribute("aria-describedby",r),e.removeAttribute("title")):(o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t))},complete(){o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t)}}),i.pipe($e(16,ye)).subscribe(({active:s})=>{o.classList.toggle("md-tooltip--active",s)}),i.pipe(gt(125,ye),g(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:s})=>s)).subscribe({next(s){s?o.style.setProperty("--md-tooltip-0",`${-s}px`):o.style.removeProperty("--md-tooltip-0")},complete(){o.style.removeProperty("--md-tooltip-0")}}),ls(o,e).pipe(O(s=>i.next(s)),A(()=>i.complete()),m(s=>P({ref:e},s)))}).pipe(et(pe))}function ms({viewport$:e}){if(!V("header.autohide"))return $(!1);let t=e.pipe(m(({offset:{y:n}})=>n),ot(2,1),m(([n,i])=>[nMath.abs(i-n.y)>100),m(([,[n]])=>n),Y()),o=Je("search");return z([e,o]).pipe(m(([{offset:n},i])=>n.y>400&&!i),Y(),b(n=>n?r:$(!1)),Q(!1))}function ai(e,t){return H(()=>z([Le(e),ms(t)])).pipe(m(([{height:r},o])=>({height:r,hidden:o})),Y((r,o)=>r.height===o.height&&r.hidden===o.hidden),Z(1))}function si(e,{header$:t,main$:r}){return H(()=>{let o=new T,n=o.pipe(oe(),ae(!0));o.pipe(ne("active"),Pe(t)).subscribe(([{active:s},{hidden:a}])=>{e.classList.toggle("md-header--shadow",s&&!a),e.hidden=a});let i=fe(M("[title]",e)).pipe(g(()=>V("content.tooltips")),J(s=>ii(s)));return r.subscribe(o),t.pipe(W(n),m(s=>P({ref:e},s)),Ve(i.pipe(W(n))))})}function fs(e,{viewport$:t,header$:r}){return Er(e,{viewport$:t,header$:r}).pipe(m(({offset:{y:o}})=>{let{height:n}=de(e);return{active:n>0&&o>=n}}),ne("active"))}function ci(e,t){return H(()=>{let r=new T;r.subscribe({next({active:n}){e.classList.toggle("md-header__title--active",n)},complete(){e.classList.remove("md-header__title--active")}});let o=ue(".md-content h1");return typeof o=="undefined"?y:fs(o,t).pipe(O(n=>r.next(n)),A(()=>r.complete()),m(n=>P({ref:e},n)))})}function pi(e,{viewport$:t,header$:r}){let o=r.pipe(m(({height:i})=>i),Y()),n=o.pipe(b(()=>Le(e).pipe(m(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),ne("bottom"))));return z([o,n,t]).pipe(m(([i,{top:s,bottom:a},{offset:{y:c},size:{height:p}}])=>(p=Math.max(0,p-Math.max(0,s-c,i)-Math.max(0,p+c-a)),{offset:s-i,height:p,active:s-i<=c})),Y((i,s)=>i.offset===s.offset&&i.height===s.height&&i.active===s.active))}function us(e){let t=__md_get("__palette")||{index:e.findIndex(o=>matchMedia(o.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return $(...e).pipe(J(o=>h(o,"change").pipe(m(()=>o))),Q(e[r]),m(o=>({index:e.indexOf(o),color:{media:o.getAttribute("data-md-color-media"),scheme:o.getAttribute("data-md-color-scheme"),primary:o.getAttribute("data-md-color-primary"),accent:o.getAttribute("data-md-color-accent")}})),Z(1))}function li(e){let t=M("input",e),r=x("meta",{name:"theme-color"});document.head.appendChild(r);let o=x("meta",{name:"color-scheme"});document.head.appendChild(o);let n=Wt("(prefers-color-scheme: light)");return H(()=>{let i=new T;return i.subscribe(s=>{if(document.body.setAttribute("data-md-color-switching",""),s.color.media==="(prefers-color-scheme)"){let a=matchMedia("(prefers-color-scheme: light)"),c=document.querySelector(a.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");s.color.scheme=c.getAttribute("data-md-color-scheme"),s.color.primary=c.getAttribute("data-md-color-primary"),s.color.accent=c.getAttribute("data-md-color-accent")}for(let[a,c]of Object.entries(s.color))document.body.setAttribute(`data-md-color-${a}`,c);for(let a=0;as.key==="Enter"),te(i,(s,a)=>a)).subscribe(({index:s})=>{s=(s+1)%t.length,t[s].click(),t[s].focus()}),i.pipe(m(()=>{let s=Ce("header"),a=window.getComputedStyle(s);return o.content=a.colorScheme,a.backgroundColor.match(/\d+/g).map(c=>(+c).toString(16).padStart(2,"0")).join("")})).subscribe(s=>r.content=`#${s}`),i.pipe(xe(pe)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),us(t).pipe(W(n.pipe(Ie(1))),vt(),O(s=>i.next(s)),A(()=>i.complete()),m(s=>P({ref:e},s)))})}function mi(e,{progress$:t}){return H(()=>{let r=new T;return r.subscribe(({value:o})=>{e.style.setProperty("--md-progress-value",`${o}`)}),t.pipe(O(o=>r.next({value:o})),A(()=>r.complete()),m(o=>({ref:e,value:o})))})}function fi(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,e}function ds(e,t){let r=new Map;for(let o of M("url",e)){let n=j("loc",o),i=[fi(new URL(n.textContent),t)];r.set(`${i[0]}`,i);for(let s of M("[rel=alternate]",o)){let a=s.getAttribute("href");a!=null&&i.push(fi(new URL(a),t))}}return r}function kt(e){return En(new URL("sitemap.xml",e)).pipe(m(t=>ds(t,new URL(e))),ve(()=>$(new Map)),le())}function ui({document$:e}){let t=new Map;e.pipe(b(()=>M("link[rel=alternate]")),m(r=>new URL(r.href)),g(r=>!t.has(r.toString())),J(r=>kt(r).pipe(m(o=>[r,o]),ve(()=>y)))).subscribe(([r,o])=>{t.set(r.toString().replace(/\/$/,""),o)}),h(document.body,"click").pipe(g(r=>!r.metaKey&&!r.ctrlKey),b(r=>{if(r.target instanceof Element){let o=r.target.closest("a");if(o&&!o.target){let n=[...t].find(([f])=>o.href.startsWith(`${f}/`));if(typeof n=="undefined")return y;let[i,s]=n,a=we();if(a.href.startsWith(i))return y;let c=Te(),p=a.href.replace(c.base,"");p=`${i}/${p}`;let l=s.has(p.split("#")[0])?new URL(p,c.base):new URL(i);return r.preventDefault(),$(l)}}return y})).subscribe(r=>st(r,!0))}var co=$t(ao());function hs(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function di({alert$:e}){co.default.isSupported()&&new F(t=>{new co.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||hs(j(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(O(t=>{t.trigger.focus()}),m(()=>Me("clipboard.copied"))).subscribe(e)}function hi(e,t){if(!(e.target instanceof Element))return y;let r=e.target.closest("a");if(r===null)return y;if(r.target||e.metaKey||e.ctrlKey)return y;let o=new URL(r.href);return o.search=o.hash="",t.has(`${o}`)?(e.preventDefault(),$(r)):y}function bi(e){let t=new Map;for(let r of M(":scope > *",e.head))t.set(r.outerHTML,r);return t}function vi(e){for(let t of M("[href], [src]",e))for(let r of["href","src"]){let o=t.getAttribute(r);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){t[r]=t[r];break}}return $(e)}function bs(e){for(let o of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...V("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let n=ue(o),i=ue(o,e);typeof n!="undefined"&&typeof i!="undefined"&&n.replaceWith(i)}let t=bi(document);for(let[o,n]of bi(e))t.has(o)?t.delete(o):document.head.appendChild(n);for(let o of t.values()){let n=o.getAttribute("name");n!=="theme-color"&&n!=="color-scheme"&&o.remove()}let r=Ce("container");return Ke(M("script",r)).pipe(b(o=>{let n=e.createElement("script");if(o.src){for(let i of o.getAttributeNames())n.setAttribute(i,o.getAttribute(i));return o.replaceWith(n),new F(i=>{n.onload=()=>i.complete()})}else return n.textContent=o.textContent,o.replaceWith(n),y}),oe(),ae(document))}function gi({sitemap$:e,location$:t,viewport$:r,progress$:o}){if(location.protocol==="file:")return y;$(document).subscribe(vi);let n=h(document.body,"click").pipe(Pe(e),b(([a,c])=>hi(a,c)),m(({href:a})=>new URL(a)),le()),i=h(window,"popstate").pipe(m(we),le());n.pipe(te(r)).subscribe(([a,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",a)}),L(n,i).subscribe(t);let s=t.pipe(ne("pathname"),b(a=>xr(a,{progress$:o}).pipe(ve(()=>(st(a,!0),y)))),b(vi),b(bs),le());return L(s.pipe(te(t,(a,c)=>c)),s.pipe(b(()=>t),ne("hash")),t.pipe(Y((a,c)=>a.pathname===c.pathname&&a.hash===c.hash),b(()=>n),O(()=>history.back()))).subscribe(a=>{var c,p;history.state!==null||!a.hash?window.scrollTo(0,(p=(c=history.state)==null?void 0:c.y)!=null?p:0):(history.scrollRestoration="auto",gn(a.hash),history.scrollRestoration="manual")}),t.subscribe(()=>{history.scrollRestoration="manual"}),h(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),r.pipe(ne("offset"),Ae(100)).subscribe(({offset:a})=>{history.replaceState(a,"")}),V("navigation.instant.prefetch")&&L(h(document.body,"mousemove"),h(document.body,"focusin")).pipe(Pe(e),b(([a,c])=>hi(a,c)),Ae(25),Qr(({href:a})=>a),hr(a=>{let c=document.createElement("link");return c.rel="prefetch",c.href=a.toString(),document.head.appendChild(c),h(c,"load").pipe(m(()=>c),Ee(1))})).subscribe(a=>a.remove()),s}var yi=$t(ro());function xi(e){let t=e.separator.split("|").map(n=>n.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":n).join("|"),r=new RegExp(t,"img"),o=(n,i,s)=>`${i}${s}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").replace(/&/g,"&").trim();let i=new RegExp(`(^|${e.separator}|)(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return s=>(0,yi.default)(s).replace(i,o).replace(/<\/mark>(\s+)]*>/img,"$1")}}function zt(e){return e.type===1}function Sr(e){return e.type===3}function Ei(e,t){let r=Mn(e);return L($(location.protocol!=="file:"),Je("search")).pipe(Re(o=>o),b(()=>t)).subscribe(({config:o,docs:n})=>r.next({type:0,data:{config:o,docs:n,options:{suggest:V("search.suggest")}}})),r}function wi(e){var l;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:o,currentBaseURL:n}=e,i=(l=po(n))==null?void 0:l.pathname;if(i===void 0)return;let s=ys(o.pathname,i);if(s===void 0)return;let a=Es(t.keys());if(!t.has(a))return;let c=po(s,a);if(!c||!t.has(c.href))return;let p=po(s,r);if(p)return p.hash=o.hash,p.search=o.search,p}function po(e,t){try{return new URL(e,t)}catch(r){return}}function ys(e,t){if(e.startsWith(t))return e.slice(t.length)}function xs(e,t){let r=Math.min(e.length,t.length),o;for(o=0;oy)),o=r.pipe(m(n=>{let[,i]=t.base.match(/([^/]+)\/?$/);return n.find(({version:s,aliases:a})=>s===i||a.includes(i))||n[0]}));r.pipe(m(n=>new Map(n.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),b(n=>h(document.body,"click").pipe(g(i=>!i.metaKey&&!i.ctrlKey),te(o),b(([i,s])=>{if(i.target instanceof Element){let a=i.target.closest("a");if(a&&!a.target&&n.has(a.href)){let c=a.href;return!i.target.closest(".md-version")&&n.get(c)===s?y:(i.preventDefault(),$(new URL(c)))}}return y}),b(i=>kt(i).pipe(m(s=>{var a;return(a=wi({selectedVersionSitemap:s,selectedVersionBaseURL:i,currentLocation:we(),currentBaseURL:t.base}))!=null?a:i})))))).subscribe(n=>st(n,!0)),z([r,o]).subscribe(([n,i])=>{j(".md-header__topic").appendChild(Wn(n,i))}),e.pipe(b(()=>o)).subscribe(n=>{var a;let i=new URL(t.base),s=__md_get("__outdated",sessionStorage,i);if(s===null){s=!0;let c=((a=t.version)==null?void 0:a.default)||"latest";Array.isArray(c)||(c=[c]);e:for(let p of c)for(let l of n.aliases.concat(n.version))if(new RegExp(p,"i").test(l)){s=!1;break e}__md_set("__outdated",s,sessionStorage,i)}if(s)for(let c of me("outdated"))c.hidden=!1})}function ws(e,{worker$:t}){let{searchParams:r}=we();r.has("q")&&(at("search",!0),e.value=r.get("q"),e.focus(),Je("search").pipe(Re(i=>!i)).subscribe(()=>{let i=we();i.searchParams.delete("q"),history.replaceState({},"",`${i}`)}));let o=Ye(e),n=L(t.pipe(Re(zt)),h(e,"keyup"),o).pipe(m(()=>e.value),Y());return z([n,o]).pipe(m(([i,s])=>({value:i,focus:s})),Z(1))}function Si(e,{worker$:t}){let r=new T,o=r.pipe(oe(),ae(!0));z([t.pipe(Re(zt)),r],(i,s)=>s).pipe(ne("value")).subscribe(({value:i})=>t.next({type:2,data:i})),r.pipe(ne("focus")).subscribe(({focus:i})=>{i&&at("search",i)}),h(e.form,"reset").pipe(W(o)).subscribe(()=>e.focus());let n=j("header [for=__search]");return h(n,"click").subscribe(()=>e.focus()),ws(e,{worker$:t}).pipe(O(i=>r.next(i)),A(()=>r.complete()),m(i=>P({ref:e},i)),Z(1))}function Oi(e,{worker$:t,query$:r}){let o=new T,n=un(e.parentElement).pipe(g(Boolean)),i=e.parentElement,s=j(":scope > :first-child",e),a=j(":scope > :last-child",e);Je("search").subscribe(l=>{a.setAttribute("role",l?"list":"presentation"),a.hidden=!l}),o.pipe(te(r),Gr(t.pipe(Re(zt)))).subscribe(([{items:l},{value:f}])=>{switch(l.length){case 0:s.textContent=f.length?Me("search.result.none"):Me("search.result.placeholder");break;case 1:s.textContent=Me("search.result.one");break;default:let u=br(l.length);s.textContent=Me("search.result.other",u)}});let c=o.pipe(O(()=>a.innerHTML=""),b(({items:l})=>L($(...l.slice(0,10)),$(...l.slice(10)).pipe(ot(4),Xr(n),b(([f])=>f)))),m(Fn),le());return c.subscribe(l=>a.appendChild(l)),c.pipe(J(l=>{let f=ue("details",l);return typeof f=="undefined"?y:h(f,"toggle").pipe(W(o),m(()=>f))})).subscribe(l=>{l.open===!1&&l.offsetTop<=i.scrollTop&&i.scrollTo({top:l.offsetTop})}),t.pipe(g(Sr),m(({data:l})=>l)).pipe(O(l=>o.next(l)),A(()=>o.complete()),m(l=>P({ref:e},l)))}function Ts(e,{query$:t}){return t.pipe(m(({value:r})=>{let o=we();return o.hash="",r=r.replace(/\s+/g,"+").replace(/&/g,"%26").replace(/=/g,"%3D"),o.search=`q=${r}`,{url:o}}))}function Li(e,t){let r=new T,o=r.pipe(oe(),ae(!0));return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),h(e,"click").pipe(W(o)).subscribe(n=>n.preventDefault()),Ts(e,t).pipe(O(n=>r.next(n)),A(()=>r.complete()),m(n=>P({ref:e},n)))}function Mi(e,{worker$:t,keyboard$:r}){let o=new T,n=Ce("search-query"),i=L(h(n,"keydown"),h(n,"focus")).pipe(xe(pe),m(()=>n.value),Y());return o.pipe(Pe(i),m(([{suggest:a},c])=>{let p=c.split(/([\s-]+)/);if(a!=null&&a.length&&p[p.length-1]){let l=a[a.length-1];l.startsWith(p[p.length-1])&&(p[p.length-1]=l)}else p.length=0;return p})).subscribe(a=>e.innerHTML=a.join("").replace(/\s/g," ")),r.pipe(g(({mode:a})=>a==="search")).subscribe(a=>{a.type==="ArrowRight"&&e.innerText.length&&n.selectionStart===n.value.length&&(n.value=e.innerText)}),t.pipe(g(Sr),m(({data:a})=>a)).pipe(O(a=>o.next(a)),A(()=>o.complete()),m(()=>({ref:e})))}function _i(e,{index$:t,keyboard$:r}){let o=Te();try{let n=Ei(o.search,t),i=Ce("search-query",e),s=Ce("search-result",e);h(e,"click").pipe(g(({target:c})=>c instanceof Element&&!!c.closest("a"))).subscribe(()=>at("search",!1)),r.pipe(g(({mode:c})=>c==="search")).subscribe(c=>{let p=Ne();switch(c.type){case"Enter":if(p===i){let l=new Map;for(let f of M(":first-child [href]",s)){let u=f.firstElementChild;l.set(f,parseFloat(u.getAttribute("data-md-score")))}if(l.size){let[[f]]=[...l].sort(([,u],[,d])=>d-u);f.click()}c.claim()}break;case"Escape":case"Tab":at("search",!1),i.blur();break;case"ArrowUp":case"ArrowDown":if(typeof p=="undefined")i.focus();else{let l=[i,...M(":not(details) > [href], summary, details[open] [href]",s)],f=Math.max(0,(Math.max(0,l.indexOf(p))+l.length+(c.type==="ArrowUp"?-1:1))%l.length);l[f].focus()}c.claim();break;default:i!==Ne()&&i.focus()}}),r.pipe(g(({mode:c})=>c==="global")).subscribe(c=>{switch(c.type){case"f":case"s":case"/":i.focus(),i.select(),c.claim();break}});let a=Si(i,{worker$:n});return L(a,Oi(s,{worker$:n,query$:a})).pipe(Ve(...me("search-share",e).map(c=>Li(c,{query$:a})),...me("search-suggest",e).map(c=>Mi(c,{worker$:n,keyboard$:r}))))}catch(n){return e.hidden=!0,tt}}function Ai(e,{index$:t,location$:r}){return z([t,r.pipe(Q(we()),g(o=>!!o.searchParams.get("h")))]).pipe(m(([o,n])=>xi(o.config)(n.searchParams.get("h"))),m(o=>{var s;let n=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let a=i.nextNode();a;a=i.nextNode())if((s=a.parentElement)!=null&&s.offsetHeight){let c=a.textContent,p=o(c);p.length>c.length&&n.set(a,p)}for(let[a,c]of n){let{childNodes:p}=x("span",null,c);a.replaceWith(...Array.from(p))}return{ref:e,nodes:n}}))}function Ss(e,{viewport$:t,main$:r}){let o=e.closest(".md-grid"),n=o.offsetTop-o.parentElement.offsetTop;return z([r,t]).pipe(m(([{offset:i,height:s},{offset:{y:a}}])=>(s=s+Math.min(n,Math.max(0,a-i))-n,{height:s,locked:a>=i+n})),Y((i,s)=>i.height===s.height&&i.locked===s.locked))}function lo(e,o){var n=o,{header$:t}=n,r=vo(n,["header$"]);let i=j(".md-sidebar__scrollwrap",e),{y:s}=Be(i);return H(()=>{let a=new T,c=a.pipe(oe(),ae(!0)),p=a.pipe($e(0,ye));return p.pipe(te(t)).subscribe({next([{height:l},{height:f}]){i.style.height=`${l-2*s}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),p.pipe(Re()).subscribe(()=>{for(let l of M(".md-nav__link--active[href]",e)){if(!l.clientHeight)continue;let f=l.closest(".md-sidebar__scrollwrap");if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:d}=de(f);f.scrollTo({top:u-d/2})}}}),fe(M("label[tabindex]",e)).pipe(J(l=>h(l,"click").pipe(xe(pe),m(()=>l),W(c)))).subscribe(l=>{let f=j(`[id="${l.htmlFor}"]`);j(`[aria-labelledby="${l.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),V("content.tooltips")&&fe(M("abbr[title]",e)).pipe(J(l=>Xe(l,{viewport$})),W(c)).subscribe(),Ss(e,r).pipe(O(l=>a.next(l)),A(()=>a.complete()),m(l=>P({ref:e},l)))})}function Ci(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return rt(ze(`${r}/releases/latest`).pipe(ve(()=>y),m(o=>({version:o.tag_name})),Qe({})),ze(r).pipe(ve(()=>y),m(o=>({stars:o.stargazers_count,forks:o.forks_count})),Qe({}))).pipe(m(([o,n])=>P(P({},o),n)))}else{let r=`https://api.github.com/users/${e}`;return ze(r).pipe(m(o=>({repositories:o.public_repos})),Qe({}))}}function ki(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return rt(ze(`${r}/releases/permalink/latest`).pipe(ve(()=>y),m(({tag_name:o})=>({version:o})),Qe({})),ze(r).pipe(ve(()=>y),m(({star_count:o,forks_count:n})=>({stars:o,forks:n})),Qe({}))).pipe(m(([o,n])=>P(P({},o),n)))}function Hi(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,o]=t;return Ci(r,o)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,o]=t;return ki(r,o)}return y}var Os;function Ls(e){return Os||(Os=H(()=>{let t=__md_get("__source",sessionStorage);if(t)return $(t);if(me("consent").length){let o=__md_get("__consent");if(!(o&&o.github))return y}return Hi(e.href).pipe(O(o=>__md_set("__source",o,sessionStorage)))}).pipe(ve(()=>y),g(t=>Object.keys(t).length>0),m(t=>({facts:t})),Z(1)))}function $i(e){let t=j(":scope > :last-child",e);return H(()=>{let r=new T;return r.subscribe(({facts:o})=>{t.appendChild(jn(o)),t.classList.add("md-source__repository--active")}),Ls(e).pipe(O(o=>r.next(o)),A(()=>r.complete()),m(o=>P({ref:e},o)))})}function Ms(e,{viewport$:t,header$:r}){return Le(document.body).pipe(b(()=>Er(e,{header$:r,viewport$:t})),m(({offset:{y:o}})=>({hidden:o>=10})),ne("hidden"))}function Pi(e,t){return H(()=>{let r=new T;return r.subscribe({next({hidden:o}){e.hidden=o},complete(){e.hidden=!1}}),(V("navigation.tabs.sticky")?$({hidden:!1}):Ms(e,t)).pipe(O(o=>r.next(o)),A(()=>r.complete()),m(o=>P({ref:e},o)))})}function _s(e,{viewport$:t,header$:r}){let o=new Map,n=M(".md-nav__link",e);for(let a of n){let c=decodeURIComponent(a.hash.substring(1)),p=ue(`[id="${c}"]`);typeof p!="undefined"&&o.set(a,p)}let i=r.pipe(ne("height"),m(({height:a})=>{let c=Ce("main"),p=j(":scope > :first-child",c);return a+.8*(p.offsetTop-c.offsetTop)}),le());return Le(document.body).pipe(ne("height"),b(a=>H(()=>{let c=[];return $([...o].reduce((p,[l,f])=>{for(;c.length&&o.get(c[c.length-1]).tagName>=f.tagName;)c.pop();let u=f.offsetTop;for(;!u&&f.parentElement;)f=f.parentElement,u=f.offsetTop;let d=f.offsetParent;for(;d;d=d.offsetParent)u+=d.offsetTop;return p.set([...c=[...c,l]].reverse(),u)},new Map))}).pipe(m(c=>new Map([...c].sort(([,p],[,l])=>p-l))),Pe(i),b(([c,p])=>t.pipe(Ut(([l,f],{offset:{y:u},size:d})=>{let v=u+d.height>=Math.floor(a.height);for(;f.length;){let[,S]=f[0];if(S-p=u&&!v)f=[l.pop(),...f];else break}return[l,f]},[[],[...c]]),Y((l,f)=>l[0]===f[0]&&l[1]===f[1])))))).pipe(m(([a,c])=>({prev:a.map(([p])=>p),next:c.map(([p])=>p)})),Q({prev:[],next:[]}),ot(2,1),m(([a,c])=>a.prev.length{let i=new T,s=i.pipe(oe(),ae(!0));if(i.subscribe(({prev:a,next:c})=>{for(let[p]of c)p.classList.remove("md-nav__link--passed"),p.classList.remove("md-nav__link--active");for(let[p,[l]]of a.entries())l.classList.add("md-nav__link--passed"),l.classList.toggle("md-nav__link--active",p===a.length-1)}),V("toc.follow")){let a=L(t.pipe(Ae(1),m(()=>{})),t.pipe(Ae(250),m(()=>"smooth")));i.pipe(g(({prev:c})=>c.length>0),Pe(o.pipe(xe(pe))),te(a)).subscribe(([[{prev:c}],p])=>{let[l]=c[c.length-1];if(l.offsetHeight){let f=vr(l);if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:d}=de(f);f.scrollTo({top:u-d/2,behavior:p})}}})}return V("navigation.tracking")&&t.pipe(W(s),ne("offset"),Ae(250),Ie(1),W(n.pipe(Ie(1))),vt({delay:250}),te(i)).subscribe(([,{prev:a}])=>{let c=we(),p=a[a.length-1];if(p&&p.length){let[l]=p,{hash:f}=new URL(l.href);c.hash!==f&&(c.hash=f,history.replaceState({},"",`${c}`))}else c.hash="",history.replaceState({},"",`${c}`)}),_s(e,{viewport$:t,header$:r}).pipe(O(a=>i.next(a)),A(()=>i.complete()),m(a=>P({ref:e},a)))})}function As(e,{viewport$:t,main$:r,target$:o}){let n=t.pipe(m(({offset:{y:s}})=>s),ot(2,1),m(([s,a])=>s>a&&a>0),Y()),i=r.pipe(m(({active:s})=>s));return z([i,n]).pipe(m(([s,a])=>!(s&&a)),Y(),W(o.pipe(Ie(1))),ae(!0),vt({delay:250}),m(s=>({hidden:s})))}function Ii(e,{viewport$:t,header$:r,main$:o,target$:n}){let i=new T,s=i.pipe(oe(),ae(!0));return i.subscribe({next({hidden:a}){e.hidden=a,a?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(W(s),ne("height")).subscribe(({height:a})=>{e.style.top=`${a+16}px`}),h(e,"click").subscribe(a=>{a.preventDefault(),window.scrollTo({top:0})}),As(e,{viewport$:t,main$:o,target$:n}).pipe(O(a=>i.next(a)),A(()=>i.complete()),m(a=>P({ref:e},a)))}function Fi({document$:e,viewport$:t}){e.pipe(b(()=>M(".md-ellipsis")),J(r=>mt(r).pipe(W(e.pipe(Ie(1))),g(o=>o),m(()=>r),Ee(1))),g(r=>r.offsetWidth{let o=r.innerText,n=r.closest("a")||r;return n.title=o,V("content.tooltips")?Xe(n,{viewport$:t}).pipe(W(e.pipe(Ie(1))),A(()=>n.removeAttribute("title"))):y})).subscribe(),V("content.tooltips")&&e.pipe(b(()=>M(".md-status")),J(r=>Xe(r,{viewport$:t}))).subscribe()}function ji({document$:e,tablet$:t}){e.pipe(b(()=>M(".md-toggle--indeterminate")),O(r=>{r.indeterminate=!0,r.checked=!1}),J(r=>h(r,"change").pipe(Jr(()=>r.classList.contains("md-toggle--indeterminate")),m(()=>r))),te(t)).subscribe(([r,o])=>{r.classList.remove("md-toggle--indeterminate"),o&&(r.checked=!1)})}function Cs(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Ui({document$:e}){e.pipe(b(()=>M("[data-md-scrollfix]")),O(t=>t.removeAttribute("data-md-scrollfix")),g(Cs),J(t=>h(t,"touchstart").pipe(m(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function Wi({viewport$:e,tablet$:t}){z([Je("search"),t]).pipe(m(([r,o])=>r&&!o),b(r=>$(r).pipe(nt(r?400:100))),te(e)).subscribe(([r,{offset:{y:o}}])=>{if(r)document.body.setAttribute("data-md-scrolllock",""),document.body.style.top=`-${o}px`;else{let n=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-scrolllock"),document.body.style.top="",n&&window.scrollTo(0,n)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let o=e[r];typeof o=="string"?o=document.createTextNode(o):o.parentNode&&o.parentNode.removeChild(o),r?t.insertBefore(this.previousSibling,o):t.replaceChild(o,this)}}}));function ks(){return location.protocol==="file:"?_t(`${new URL("search/search_index.js",Or.base)}`).pipe(m(()=>__index),Z(1)):ze(new URL("search/search_index.json",Or.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var ct=an(),Kt=bn(),Ht=yn(Kt),mo=hn(),ke=Ln(),Lr=Wt("(min-width: 60em)"),Vi=Wt("(min-width: 76.25em)"),Ni=xn(),Or=Te(),zi=document.forms.namedItem("search")?ks():tt,fo=new T;di({alert$:fo});ui({document$:ct});var uo=new T,qi=kt(Or.base);V("navigation.instant")&&gi({sitemap$:qi,location$:Kt,viewport$:ke,progress$:uo}).subscribe(ct);var Di;((Di=Or.version)==null?void 0:Di.provider)==="mike"&&Ti({document$:ct});L(Kt,Ht).pipe(nt(125)).subscribe(()=>{at("drawer",!1),at("search",!1)});mo.pipe(g(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=ue("link[rel=prev]");typeof t!="undefined"&&st(t);break;case"n":case".":let r=ue("link[rel=next]");typeof r!="undefined"&&st(r);break;case"Enter":let o=Ne();o instanceof HTMLLabelElement&&o.click()}});Fi({viewport$:ke,document$:ct});ji({document$:ct,tablet$:Lr});Ui({document$:ct});Wi({viewport$:ke,tablet$:Lr});var ft=ai(Ce("header"),{viewport$:ke}),qt=ct.pipe(m(()=>Ce("main")),b(e=>pi(e,{viewport$:ke,header$:ft})),Z(1)),Hs=L(...me("consent").map(e=>An(e,{target$:Ht})),...me("dialog").map(e=>ni(e,{alert$:fo})),...me("palette").map(e=>li(e)),...me("progress").map(e=>mi(e,{progress$:uo})),...me("search").map(e=>_i(e,{index$:zi,keyboard$:mo})),...me("source").map(e=>$i(e))),$s=H(()=>L(...me("announce").map(e=>_n(e)),...me("content").map(e=>oi(e,{sitemap$:qi,viewport$:ke,target$:Ht,print$:Ni})),...me("content").map(e=>V("search.highlight")?Ai(e,{index$:zi,location$:Kt}):y),...me("header").map(e=>si(e,{viewport$:ke,header$:ft,main$:qt})),...me("header-title").map(e=>ci(e,{viewport$:ke,header$:ft})),...me("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?eo(Vi,()=>lo(e,{viewport$:ke,header$:ft,main$:qt})):eo(Lr,()=>lo(e,{viewport$:ke,header$:ft,main$:qt}))),...me("tabs").map(e=>Pi(e,{viewport$:ke,header$:ft})),...me("toc").map(e=>Ri(e,{viewport$:ke,header$:ft,main$:qt,target$:Ht})),...me("top").map(e=>Ii(e,{viewport$:ke,header$:ft,main$:qt,target$:Ht})))),Ki=ct.pipe(b(()=>$s),Ve(Hs),Z(1));Ki.subscribe();window.document$=ct;window.location$=Kt;window.target$=Ht;window.keyboard$=mo;window.viewport$=ke;window.tablet$=Lr;window.screen$=Vi;window.print$=Ni;window.alert$=fo;window.progress$=uo;window.component$=Ki;})(); +//# sourceMappingURL=bundle.79ae519e.min.js.map + diff --git a/site/assets/javascripts/bundle.79ae519e.min.js.map b/site/assets/javascripts/bundle.79ae519e.min.js.map new file mode 100644 index 00000000..5cf02892 --- /dev/null +++ b/site/assets/javascripts/bundle.79ae519e.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["node_modules/focus-visible/dist/focus-visible.js", "node_modules/escape-html/index.js", "node_modules/clipboard/dist/clipboard.js", "src/templates/assets/javascripts/bundle.ts", "node_modules/tslib/tslib.es6.mjs", "node_modules/rxjs/src/internal/util/isFunction.ts", "node_modules/rxjs/src/internal/util/createErrorClass.ts", "node_modules/rxjs/src/internal/util/UnsubscriptionError.ts", "node_modules/rxjs/src/internal/util/arrRemove.ts", "node_modules/rxjs/src/internal/Subscription.ts", "node_modules/rxjs/src/internal/config.ts", "node_modules/rxjs/src/internal/scheduler/timeoutProvider.ts", "node_modules/rxjs/src/internal/util/reportUnhandledError.ts", "node_modules/rxjs/src/internal/util/noop.ts", "node_modules/rxjs/src/internal/NotificationFactories.ts", "node_modules/rxjs/src/internal/util/errorContext.ts", "node_modules/rxjs/src/internal/Subscriber.ts", "node_modules/rxjs/src/internal/symbol/observable.ts", "node_modules/rxjs/src/internal/util/identity.ts", "node_modules/rxjs/src/internal/util/pipe.ts", "node_modules/rxjs/src/internal/Observable.ts", "node_modules/rxjs/src/internal/util/lift.ts", "node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts", "node_modules/rxjs/src/internal/scheduler/animationFrameProvider.ts", "node_modules/rxjs/src/internal/util/ObjectUnsubscribedError.ts", "node_modules/rxjs/src/internal/Subject.ts", "node_modules/rxjs/src/internal/BehaviorSubject.ts", "node_modules/rxjs/src/internal/scheduler/dateTimestampProvider.ts", "node_modules/rxjs/src/internal/ReplaySubject.ts", "node_modules/rxjs/src/internal/scheduler/Action.ts", "node_modules/rxjs/src/internal/scheduler/intervalProvider.ts", "node_modules/rxjs/src/internal/scheduler/AsyncAction.ts", "node_modules/rxjs/src/internal/Scheduler.ts", "node_modules/rxjs/src/internal/scheduler/AsyncScheduler.ts", "node_modules/rxjs/src/internal/scheduler/async.ts", "node_modules/rxjs/src/internal/scheduler/QueueAction.ts", "node_modules/rxjs/src/internal/scheduler/QueueScheduler.ts", "node_modules/rxjs/src/internal/scheduler/queue.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameAction.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameScheduler.ts", "node_modules/rxjs/src/internal/scheduler/animationFrame.ts", "node_modules/rxjs/src/internal/observable/empty.ts", "node_modules/rxjs/src/internal/util/isScheduler.ts", "node_modules/rxjs/src/internal/util/args.ts", "node_modules/rxjs/src/internal/util/isArrayLike.ts", "node_modules/rxjs/src/internal/util/isPromise.ts", "node_modules/rxjs/src/internal/util/isInteropObservable.ts", "node_modules/rxjs/src/internal/util/isAsyncIterable.ts", "node_modules/rxjs/src/internal/util/throwUnobservableError.ts", "node_modules/rxjs/src/internal/symbol/iterator.ts", "node_modules/rxjs/src/internal/util/isIterable.ts", "node_modules/rxjs/src/internal/util/isReadableStreamLike.ts", "node_modules/rxjs/src/internal/observable/innerFrom.ts", "node_modules/rxjs/src/internal/util/executeSchedule.ts", "node_modules/rxjs/src/internal/operators/observeOn.ts", "node_modules/rxjs/src/internal/operators/subscribeOn.ts", "node_modules/rxjs/src/internal/scheduled/scheduleObservable.ts", "node_modules/rxjs/src/internal/scheduled/schedulePromise.ts", "node_modules/rxjs/src/internal/scheduled/scheduleArray.ts", "node_modules/rxjs/src/internal/scheduled/scheduleIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleAsyncIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleReadableStreamLike.ts", "node_modules/rxjs/src/internal/scheduled/scheduled.ts", "node_modules/rxjs/src/internal/observable/from.ts", "node_modules/rxjs/src/internal/observable/of.ts", "node_modules/rxjs/src/internal/observable/throwError.ts", "node_modules/rxjs/src/internal/util/EmptyError.ts", "node_modules/rxjs/src/internal/util/isDate.ts", "node_modules/rxjs/src/internal/operators/map.ts", "node_modules/rxjs/src/internal/util/mapOneOrManyArgs.ts", "node_modules/rxjs/src/internal/util/argsArgArrayOrObject.ts", "node_modules/rxjs/src/internal/util/createObject.ts", "node_modules/rxjs/src/internal/observable/combineLatest.ts", "node_modules/rxjs/src/internal/operators/mergeInternals.ts", "node_modules/rxjs/src/internal/operators/mergeMap.ts", "node_modules/rxjs/src/internal/operators/mergeAll.ts", "node_modules/rxjs/src/internal/operators/concatAll.ts", "node_modules/rxjs/src/internal/observable/concat.ts", "node_modules/rxjs/src/internal/observable/defer.ts", "node_modules/rxjs/src/internal/observable/fromEvent.ts", "node_modules/rxjs/src/internal/observable/fromEventPattern.ts", "node_modules/rxjs/src/internal/observable/timer.ts", "node_modules/rxjs/src/internal/observable/merge.ts", "node_modules/rxjs/src/internal/observable/never.ts", "node_modules/rxjs/src/internal/util/argsOrArgArray.ts", "node_modules/rxjs/src/internal/operators/filter.ts", "node_modules/rxjs/src/internal/observable/zip.ts", "node_modules/rxjs/src/internal/operators/audit.ts", "node_modules/rxjs/src/internal/operators/auditTime.ts", "node_modules/rxjs/src/internal/operators/bufferCount.ts", "node_modules/rxjs/src/internal/operators/catchError.ts", "node_modules/rxjs/src/internal/operators/scanInternals.ts", "node_modules/rxjs/src/internal/operators/combineLatest.ts", "node_modules/rxjs/src/internal/operators/combineLatestWith.ts", "node_modules/rxjs/src/internal/operators/debounce.ts", "node_modules/rxjs/src/internal/operators/debounceTime.ts", "node_modules/rxjs/src/internal/operators/defaultIfEmpty.ts", "node_modules/rxjs/src/internal/operators/take.ts", "node_modules/rxjs/src/internal/operators/ignoreElements.ts", "node_modules/rxjs/src/internal/operators/mapTo.ts", "node_modules/rxjs/src/internal/operators/delayWhen.ts", "node_modules/rxjs/src/internal/operators/delay.ts", "node_modules/rxjs/src/internal/operators/distinct.ts", "node_modules/rxjs/src/internal/operators/distinctUntilChanged.ts", "node_modules/rxjs/src/internal/operators/distinctUntilKeyChanged.ts", "node_modules/rxjs/src/internal/operators/throwIfEmpty.ts", "node_modules/rxjs/src/internal/operators/endWith.ts", "node_modules/rxjs/src/internal/operators/exhaustMap.ts", "node_modules/rxjs/src/internal/operators/finalize.ts", "node_modules/rxjs/src/internal/operators/first.ts", "node_modules/rxjs/src/internal/operators/takeLast.ts", "node_modules/rxjs/src/internal/operators/merge.ts", "node_modules/rxjs/src/internal/operators/mergeWith.ts", "node_modules/rxjs/src/internal/operators/repeat.ts", "node_modules/rxjs/src/internal/operators/scan.ts", "node_modules/rxjs/src/internal/operators/share.ts", "node_modules/rxjs/src/internal/operators/shareReplay.ts", "node_modules/rxjs/src/internal/operators/skip.ts", "node_modules/rxjs/src/internal/operators/skipUntil.ts", "node_modules/rxjs/src/internal/operators/startWith.ts", "node_modules/rxjs/src/internal/operators/switchMap.ts", "node_modules/rxjs/src/internal/operators/takeUntil.ts", "node_modules/rxjs/src/internal/operators/takeWhile.ts", "node_modules/rxjs/src/internal/operators/tap.ts", "node_modules/rxjs/src/internal/operators/throttle.ts", "node_modules/rxjs/src/internal/operators/throttleTime.ts", "node_modules/rxjs/src/internal/operators/withLatestFrom.ts", "node_modules/rxjs/src/internal/operators/zip.ts", "node_modules/rxjs/src/internal/operators/zipWith.ts", "src/templates/assets/javascripts/browser/document/index.ts", "src/templates/assets/javascripts/browser/element/_/index.ts", "src/templates/assets/javascripts/browser/element/focus/index.ts", "src/templates/assets/javascripts/browser/element/hover/index.ts", "src/templates/assets/javascripts/utilities/h/index.ts", "src/templates/assets/javascripts/utilities/round/index.ts", "src/templates/assets/javascripts/browser/script/index.ts", "src/templates/assets/javascripts/browser/element/size/_/index.ts", "src/templates/assets/javascripts/browser/element/size/content/index.ts", "src/templates/assets/javascripts/browser/element/offset/_/index.ts", "src/templates/assets/javascripts/browser/element/offset/content/index.ts", "src/templates/assets/javascripts/browser/element/visibility/index.ts", "src/templates/assets/javascripts/browser/toggle/index.ts", "src/templates/assets/javascripts/browser/keyboard/index.ts", "src/templates/assets/javascripts/browser/location/_/index.ts", "src/templates/assets/javascripts/browser/location/hash/index.ts", "src/templates/assets/javascripts/browser/media/index.ts", "src/templates/assets/javascripts/browser/request/index.ts", "src/templates/assets/javascripts/browser/viewport/offset/index.ts", "src/templates/assets/javascripts/browser/viewport/size/index.ts", "src/templates/assets/javascripts/browser/viewport/_/index.ts", "src/templates/assets/javascripts/browser/viewport/at/index.ts", "src/templates/assets/javascripts/browser/worker/index.ts", "src/templates/assets/javascripts/_/index.ts", "src/templates/assets/javascripts/components/_/index.ts", "src/templates/assets/javascripts/components/announce/index.ts", "src/templates/assets/javascripts/components/consent/index.ts", "src/templates/assets/javascripts/templates/tooltip/index.tsx", "src/templates/assets/javascripts/templates/annotation/index.tsx", "src/templates/assets/javascripts/templates/clipboard/index.tsx", "src/templates/assets/javascripts/templates/search/index.tsx", "src/templates/assets/javascripts/templates/source/index.tsx", "src/templates/assets/javascripts/templates/tabbed/index.tsx", "src/templates/assets/javascripts/templates/table/index.tsx", "src/templates/assets/javascripts/templates/version/index.tsx", "src/templates/assets/javascripts/components/tooltip2/index.ts", "src/templates/assets/javascripts/components/content/annotation/_/index.ts", "src/templates/assets/javascripts/components/content/annotation/list/index.ts", "src/templates/assets/javascripts/components/content/annotation/block/index.ts", "src/templates/assets/javascripts/components/content/code/_/index.ts", "src/templates/assets/javascripts/components/content/details/index.ts", "src/templates/assets/javascripts/components/content/link/index.ts", "src/templates/assets/javascripts/components/content/mermaid/index.css", "src/templates/assets/javascripts/components/content/mermaid/index.ts", "src/templates/assets/javascripts/components/content/table/index.ts", "src/templates/assets/javascripts/components/content/tabs/index.ts", "src/templates/assets/javascripts/components/content/_/index.ts", "src/templates/assets/javascripts/components/dialog/index.ts", "src/templates/assets/javascripts/components/tooltip/index.ts", "src/templates/assets/javascripts/components/header/_/index.ts", "src/templates/assets/javascripts/components/header/title/index.ts", "src/templates/assets/javascripts/components/main/index.ts", "src/templates/assets/javascripts/components/palette/index.ts", "src/templates/assets/javascripts/components/progress/index.ts", "src/templates/assets/javascripts/integrations/sitemap/index.ts", "src/templates/assets/javascripts/integrations/alternate/index.ts", "src/templates/assets/javascripts/integrations/clipboard/index.ts", "src/templates/assets/javascripts/integrations/instant/index.ts", "src/templates/assets/javascripts/integrations/search/highlighter/index.ts", "src/templates/assets/javascripts/integrations/search/worker/message/index.ts", "src/templates/assets/javascripts/integrations/search/worker/_/index.ts", "src/templates/assets/javascripts/integrations/version/findurl/index.ts", "src/templates/assets/javascripts/integrations/version/index.ts", "src/templates/assets/javascripts/components/search/query/index.ts", "src/templates/assets/javascripts/components/search/result/index.ts", "src/templates/assets/javascripts/components/search/share/index.ts", "src/templates/assets/javascripts/components/search/suggest/index.ts", "src/templates/assets/javascripts/components/search/_/index.ts", "src/templates/assets/javascripts/components/search/highlight/index.ts", "src/templates/assets/javascripts/components/sidebar/index.ts", "src/templates/assets/javascripts/components/source/facts/github/index.ts", "src/templates/assets/javascripts/components/source/facts/gitlab/index.ts", "src/templates/assets/javascripts/components/source/facts/_/index.ts", "src/templates/assets/javascripts/components/source/_/index.ts", "src/templates/assets/javascripts/components/tabs/index.ts", "src/templates/assets/javascripts/components/toc/index.ts", "src/templates/assets/javascripts/components/top/index.ts", "src/templates/assets/javascripts/patches/ellipsis/index.ts", "src/templates/assets/javascripts/patches/indeterminate/index.ts", "src/templates/assets/javascripts/patches/scrollfix/index.ts", "src/templates/assets/javascripts/patches/scrolllock/index.ts", "src/templates/assets/javascripts/polyfills/index.ts"], + "sourcesContent": ["(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (factory());\n}(this, (function () { 'use strict';\n\n /**\n * Applies the :focus-visible polyfill at the given scope.\n * A scope in this case is either the top-level Document or a Shadow Root.\n *\n * @param {(Document|ShadowRoot)} scope\n * @see https://github.com/WICG/focus-visible\n */\n function applyFocusVisiblePolyfill(scope) {\n var hadKeyboardEvent = true;\n var hadFocusVisibleRecently = false;\n var hadFocusVisibleRecentlyTimeout = null;\n\n var inputTypesAllowlist = {\n text: true,\n search: true,\n url: true,\n tel: true,\n email: true,\n password: true,\n number: true,\n date: true,\n month: true,\n week: true,\n time: true,\n datetime: true,\n 'datetime-local': true\n };\n\n /**\n * Helper function for legacy browsers and iframes which sometimes focus\n * elements like document, body, and non-interactive SVG.\n * @param {Element} el\n */\n function isValidFocusTarget(el) {\n if (\n el &&\n el !== document &&\n el.nodeName !== 'HTML' &&\n el.nodeName !== 'BODY' &&\n 'classList' in el &&\n 'contains' in el.classList\n ) {\n return true;\n }\n return false;\n }\n\n /**\n * Computes whether the given element should automatically trigger the\n * `focus-visible` class being added, i.e. whether it should always match\n * `:focus-visible` when focused.\n * @param {Element} el\n * @return {boolean}\n */\n function focusTriggersKeyboardModality(el) {\n var type = el.type;\n var tagName = el.tagName;\n\n if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) {\n return true;\n }\n\n if (tagName === 'TEXTAREA' && !el.readOnly) {\n return true;\n }\n\n if (el.isContentEditable) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Add the `focus-visible` class to the given element if it was not added by\n * the author.\n * @param {Element} el\n */\n function addFocusVisibleClass(el) {\n if (el.classList.contains('focus-visible')) {\n return;\n }\n el.classList.add('focus-visible');\n el.setAttribute('data-focus-visible-added', '');\n }\n\n /**\n * Remove the `focus-visible` class from the given element if it was not\n * originally added by the author.\n * @param {Element} el\n */\n function removeFocusVisibleClass(el) {\n if (!el.hasAttribute('data-focus-visible-added')) {\n return;\n }\n el.classList.remove('focus-visible');\n el.removeAttribute('data-focus-visible-added');\n }\n\n /**\n * If the most recent user interaction was via the keyboard;\n * and the key press did not include a meta, alt/option, or control key;\n * then the modality is keyboard. Otherwise, the modality is not keyboard.\n * Apply `focus-visible` to any current active element and keep track\n * of our keyboard modality state with `hadKeyboardEvent`.\n * @param {KeyboardEvent} e\n */\n function onKeyDown(e) {\n if (e.metaKey || e.altKey || e.ctrlKey) {\n return;\n }\n\n if (isValidFocusTarget(scope.activeElement)) {\n addFocusVisibleClass(scope.activeElement);\n }\n\n hadKeyboardEvent = true;\n }\n\n /**\n * If at any point a user clicks with a pointing device, ensure that we change\n * the modality away from keyboard.\n * This avoids the situation where a user presses a key on an already focused\n * element, and then clicks on a different element, focusing it with a\n * pointing device, while we still think we're in keyboard modality.\n * @param {Event} e\n */\n function onPointerDown(e) {\n hadKeyboardEvent = false;\n }\n\n /**\n * On `focus`, add the `focus-visible` class to the target if:\n * - the target received focus as a result of keyboard navigation, or\n * - the event target is an element that will likely require interaction\n * via the keyboard (e.g. a text box)\n * @param {Event} e\n */\n function onFocus(e) {\n // Prevent IE from focusing the document or HTML element.\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {\n addFocusVisibleClass(e.target);\n }\n }\n\n /**\n * On `blur`, remove the `focus-visible` class from the target.\n * @param {Event} e\n */\n function onBlur(e) {\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (\n e.target.classList.contains('focus-visible') ||\n e.target.hasAttribute('data-focus-visible-added')\n ) {\n // To detect a tab/window switch, we look for a blur event followed\n // rapidly by a visibility change.\n // If we don't see a visibility change within 100ms, it's probably a\n // regular focus change.\n hadFocusVisibleRecently = true;\n window.clearTimeout(hadFocusVisibleRecentlyTimeout);\n hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {\n hadFocusVisibleRecently = false;\n }, 100);\n removeFocusVisibleClass(e.target);\n }\n }\n\n /**\n * If the user changes tabs, keep track of whether or not the previously\n * focused element had .focus-visible.\n * @param {Event} e\n */\n function onVisibilityChange(e) {\n if (document.visibilityState === 'hidden') {\n // If the tab becomes active again, the browser will handle calling focus\n // on the element (Safari actually calls it twice).\n // If this tab change caused a blur on an element with focus-visible,\n // re-apply the class when the user switches back to the tab.\n if (hadFocusVisibleRecently) {\n hadKeyboardEvent = true;\n }\n addInitialPointerMoveListeners();\n }\n }\n\n /**\n * Add a group of listeners to detect usage of any pointing devices.\n * These listeners will be added when the polyfill first loads, and anytime\n * the window is blurred, so that they are active when the window regains\n * focus.\n */\n function addInitialPointerMoveListeners() {\n document.addEventListener('mousemove', onInitialPointerMove);\n document.addEventListener('mousedown', onInitialPointerMove);\n document.addEventListener('mouseup', onInitialPointerMove);\n document.addEventListener('pointermove', onInitialPointerMove);\n document.addEventListener('pointerdown', onInitialPointerMove);\n document.addEventListener('pointerup', onInitialPointerMove);\n document.addEventListener('touchmove', onInitialPointerMove);\n document.addEventListener('touchstart', onInitialPointerMove);\n document.addEventListener('touchend', onInitialPointerMove);\n }\n\n function removeInitialPointerMoveListeners() {\n document.removeEventListener('mousemove', onInitialPointerMove);\n document.removeEventListener('mousedown', onInitialPointerMove);\n document.removeEventListener('mouseup', onInitialPointerMove);\n document.removeEventListener('pointermove', onInitialPointerMove);\n document.removeEventListener('pointerdown', onInitialPointerMove);\n document.removeEventListener('pointerup', onInitialPointerMove);\n document.removeEventListener('touchmove', onInitialPointerMove);\n document.removeEventListener('touchstart', onInitialPointerMove);\n document.removeEventListener('touchend', onInitialPointerMove);\n }\n\n /**\n * When the polfyill first loads, assume the user is in keyboard modality.\n * If any event is received from a pointing device (e.g. mouse, pointer,\n * touch), turn off keyboard modality.\n * This accounts for situations where focus enters the page from the URL bar.\n * @param {Event} e\n */\n function onInitialPointerMove(e) {\n // Work around a Safari quirk that fires a mousemove on whenever the\n // window blurs, even if you're tabbing out of the page. \u00AF\\_(\u30C4)_/\u00AF\n if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {\n return;\n }\n\n hadKeyboardEvent = false;\n removeInitialPointerMoveListeners();\n }\n\n // For some kinds of state, we are interested in changes at the global scope\n // only. For example, global pointer input, global key presses and global\n // visibility change should affect the state at every scope:\n document.addEventListener('keydown', onKeyDown, true);\n document.addEventListener('mousedown', onPointerDown, true);\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('touchstart', onPointerDown, true);\n document.addEventListener('visibilitychange', onVisibilityChange, true);\n\n addInitialPointerMoveListeners();\n\n // For focus and blur, we specifically care about state changes in the local\n // scope. This is because focus / blur events that originate from within a\n // shadow root are not re-dispatched from the host element if it was already\n // the active element in its own scope:\n scope.addEventListener('focus', onFocus, true);\n scope.addEventListener('blur', onBlur, true);\n\n // We detect that a node is a ShadowRoot by ensuring that it is a\n // DocumentFragment and also has a host property. This check covers native\n // implementation and polyfill implementation transparently. If we only cared\n // about the native implementation, we could just check if the scope was\n // an instance of a ShadowRoot.\n if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {\n // Since a ShadowRoot is a special kind of DocumentFragment, it does not\n // have a root element to add a class to. So, we add this attribute to the\n // host element instead:\n scope.host.setAttribute('data-js-focus-visible', '');\n } else if (scope.nodeType === Node.DOCUMENT_NODE) {\n document.documentElement.classList.add('js-focus-visible');\n document.documentElement.setAttribute('data-js-focus-visible', '');\n }\n }\n\n // It is important to wrap all references to global window and document in\n // these checks to support server-side rendering use cases\n // @see https://github.com/WICG/focus-visible/issues/199\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Make the polyfill helper globally available. This can be used as a signal\n // to interested libraries that wish to coordinate with the polyfill for e.g.,\n // applying the polyfill to a shadow root:\n window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;\n\n // Notify interested libraries of the polyfill's presence, in case the\n // polyfill was loaded lazily:\n var event;\n\n try {\n event = new CustomEvent('focus-visible-polyfill-ready');\n } catch (error) {\n // IE11 does not support using CustomEvent as a constructor directly:\n event = document.createEvent('CustomEvent');\n event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});\n }\n\n window.dispatchEvent(event);\n }\n\n if (typeof document !== 'undefined') {\n // Apply the polyfill to the global document, so that no JavaScript\n // coordination is required to use the polyfill in the top-level document:\n applyFocusVisiblePolyfill(document);\n }\n\n})));\n", "/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n", "/*!\n * clipboard.js v2.0.11\n * https://clipboardjs.com/\n *\n * Licensed MIT \u00A9 Zeno Rocha\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClipboardJS\"] = factory();\n\telse\n\t\troot[\"ClipboardJS\"] = factory();\n})(this, function() {\nreturn /******/ (function() { // webpackBootstrap\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 686:\n/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n \"default\": function() { return /* binding */ clipboard; }\n});\n\n// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js\nvar tiny_emitter = __webpack_require__(279);\nvar tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter);\n// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js\nvar listen = __webpack_require__(370);\nvar listen_default = /*#__PURE__*/__webpack_require__.n(listen);\n// EXTERNAL MODULE: ./node_modules/select/src/select.js\nvar src_select = __webpack_require__(817);\nvar select_default = /*#__PURE__*/__webpack_require__.n(src_select);\n;// CONCATENATED MODULE: ./src/common/command.js\n/**\n * Executes a given operation type.\n * @param {String} type\n * @return {Boolean}\n */\nfunction command(type) {\n try {\n return document.execCommand(type);\n } catch (err) {\n return false;\n }\n}\n;// CONCATENATED MODULE: ./src/actions/cut.js\n\n\n/**\n * Cut action wrapper.\n * @param {String|HTMLElement} target\n * @return {String}\n */\n\nvar ClipboardActionCut = function ClipboardActionCut(target) {\n var selectedText = select_default()(target);\n command('cut');\n return selectedText;\n};\n\n/* harmony default export */ var actions_cut = (ClipboardActionCut);\n;// CONCATENATED MODULE: ./src/common/create-fake-element.js\n/**\n * Creates a fake textarea element with a value.\n * @param {String} value\n * @return {HTMLElement}\n */\nfunction createFakeElement(value) {\n var isRTL = document.documentElement.getAttribute('dir') === 'rtl';\n var fakeElement = document.createElement('textarea'); // Prevent zooming on iOS\n\n fakeElement.style.fontSize = '12pt'; // Reset box model\n\n fakeElement.style.border = '0';\n fakeElement.style.padding = '0';\n fakeElement.style.margin = '0'; // Move element out of screen horizontally\n\n fakeElement.style.position = 'absolute';\n fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px'; // Move element to the same position vertically\n\n var yPosition = window.pageYOffset || document.documentElement.scrollTop;\n fakeElement.style.top = \"\".concat(yPosition, \"px\");\n fakeElement.setAttribute('readonly', '');\n fakeElement.value = value;\n return fakeElement;\n}\n;// CONCATENATED MODULE: ./src/actions/copy.js\n\n\n\n/**\n * Create fake copy action wrapper using a fake element.\n * @param {String} target\n * @param {Object} options\n * @return {String}\n */\n\nvar fakeCopyAction = function fakeCopyAction(value, options) {\n var fakeElement = createFakeElement(value);\n options.container.appendChild(fakeElement);\n var selectedText = select_default()(fakeElement);\n command('copy');\n fakeElement.remove();\n return selectedText;\n};\n/**\n * Copy action wrapper.\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @return {String}\n */\n\n\nvar ClipboardActionCopy = function ClipboardActionCopy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n var selectedText = '';\n\n if (typeof target === 'string') {\n selectedText = fakeCopyAction(target, options);\n } else if (target instanceof HTMLInputElement && !['text', 'search', 'url', 'tel', 'password'].includes(target === null || target === void 0 ? void 0 : target.type)) {\n // If input type doesn't support `setSelectionRange`. Simulate it. https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange\n selectedText = fakeCopyAction(target.value, options);\n } else {\n selectedText = select_default()(target);\n command('copy');\n }\n\n return selectedText;\n};\n\n/* harmony default export */ var actions_copy = (ClipboardActionCopy);\n;// CONCATENATED MODULE: ./src/actions/default.js\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\n\n\n/**\n * Inner function which performs selection from either `text` or `target`\n * properties and then executes copy or cut operations.\n * @param {Object} options\n */\n\nvar ClipboardActionDefault = function ClipboardActionDefault() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n // Defines base properties passed from constructor.\n var _options$action = options.action,\n action = _options$action === void 0 ? 'copy' : _options$action,\n container = options.container,\n target = options.target,\n text = options.text; // Sets the `action` to be performed which can be either 'copy' or 'cut'.\n\n if (action !== 'copy' && action !== 'cut') {\n throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');\n } // Sets the `target` property using an element that will be have its content copied.\n\n\n if (target !== undefined) {\n if (target && _typeof(target) === 'object' && target.nodeType === 1) {\n if (action === 'copy' && target.hasAttribute('disabled')) {\n throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');\n }\n\n if (action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {\n throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes');\n }\n } else {\n throw new Error('Invalid \"target\" value, use a valid Element');\n }\n } // Define selection strategy based on `text` property.\n\n\n if (text) {\n return actions_copy(text, {\n container: container\n });\n } // Defines which selection strategy based on `target` property.\n\n\n if (target) {\n return action === 'cut' ? actions_cut(target) : actions_copy(target, {\n container: container\n });\n }\n};\n\n/* harmony default export */ var actions_default = (ClipboardActionDefault);\n;// CONCATENATED MODULE: ./src/clipboard.js\nfunction clipboard_typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { clipboard_typeof = function _typeof(obj) { return typeof obj; }; } else { clipboard_typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return clipboard_typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (clipboard_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\n\n\n/**\n * Helper function to retrieve attribute value.\n * @param {String} suffix\n * @param {Element} element\n */\n\nfunction getAttributeValue(suffix, element) {\n var attribute = \"data-clipboard-\".concat(suffix);\n\n if (!element.hasAttribute(attribute)) {\n return;\n }\n\n return element.getAttribute(attribute);\n}\n/**\n * Base class which takes one or more elements, adds event listeners to them,\n * and instantiates a new `ClipboardAction` on each click.\n */\n\n\nvar Clipboard = /*#__PURE__*/function (_Emitter) {\n _inherits(Clipboard, _Emitter);\n\n var _super = _createSuper(Clipboard);\n\n /**\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n * @param {Object} options\n */\n function Clipboard(trigger, options) {\n var _this;\n\n _classCallCheck(this, Clipboard);\n\n _this = _super.call(this);\n\n _this.resolveOptions(options);\n\n _this.listenClick(trigger);\n\n return _this;\n }\n /**\n * Defines if attributes would be resolved using internal setter functions\n * or custom functions that were passed in the constructor.\n * @param {Object} options\n */\n\n\n _createClass(Clipboard, [{\n key: \"resolveOptions\",\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n this.action = typeof options.action === 'function' ? options.action : this.defaultAction;\n this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;\n this.text = typeof options.text === 'function' ? options.text : this.defaultText;\n this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body;\n }\n /**\n * Adds a click event listener to the passed trigger.\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n */\n\n }, {\n key: \"listenClick\",\n value: function listenClick(trigger) {\n var _this2 = this;\n\n this.listener = listen_default()(trigger, 'click', function (e) {\n return _this2.onClick(e);\n });\n }\n /**\n * Defines a new `ClipboardAction` on each click event.\n * @param {Event} e\n */\n\n }, {\n key: \"onClick\",\n value: function onClick(e) {\n var trigger = e.delegateTarget || e.currentTarget;\n var action = this.action(trigger) || 'copy';\n var text = actions_default({\n action: action,\n container: this.container,\n target: this.target(trigger),\n text: this.text(trigger)\n }); // Fires an event based on the copy operation result.\n\n this.emit(text ? 'success' : 'error', {\n action: action,\n text: text,\n trigger: trigger,\n clearSelection: function clearSelection() {\n if (trigger) {\n trigger.focus();\n }\n\n window.getSelection().removeAllRanges();\n }\n });\n }\n /**\n * Default `action` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultAction\",\n value: function defaultAction(trigger) {\n return getAttributeValue('action', trigger);\n }\n /**\n * Default `target` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultTarget\",\n value: function defaultTarget(trigger) {\n var selector = getAttributeValue('target', trigger);\n\n if (selector) {\n return document.querySelector(selector);\n }\n }\n /**\n * Allow fire programmatically a copy action\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @returns Text copied.\n */\n\n }, {\n key: \"defaultText\",\n\n /**\n * Default `text` lookup function.\n * @param {Element} trigger\n */\n value: function defaultText(trigger) {\n return getAttributeValue('text', trigger);\n }\n /**\n * Destroy lifecycle.\n */\n\n }, {\n key: \"destroy\",\n value: function destroy() {\n this.listener.destroy();\n }\n }], [{\n key: \"copy\",\n value: function copy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n return actions_copy(target, options);\n }\n /**\n * Allow fire programmatically a cut action\n * @param {String|HTMLElement} target\n * @returns Text cutted.\n */\n\n }, {\n key: \"cut\",\n value: function cut(target) {\n return actions_cut(target);\n }\n /**\n * Returns the support of the given action, or all actions if no action is\n * given.\n * @param {String} [action]\n */\n\n }, {\n key: \"isSupported\",\n value: function isSupported() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];\n var actions = typeof action === 'string' ? [action] : action;\n var support = !!document.queryCommandSupported;\n actions.forEach(function (action) {\n support = support && !!document.queryCommandSupported(action);\n });\n return support;\n }\n }]);\n\n return Clipboard;\n}((tiny_emitter_default()));\n\n/* harmony default export */ var clipboard = (Clipboard);\n\n/***/ }),\n\n/***/ 828:\n/***/ (function(module) {\n\nvar DOCUMENT_NODE_TYPE = 9;\n\n/**\n * A polyfill for Element.matches()\n */\nif (typeof Element !== 'undefined' && !Element.prototype.matches) {\n var proto = Element.prototype;\n\n proto.matches = proto.matchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector ||\n proto.webkitMatchesSelector;\n}\n\n/**\n * Finds the closest parent that matches a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @return {Function}\n */\nfunction closest (element, selector) {\n while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {\n if (typeof element.matches === 'function' &&\n element.matches(selector)) {\n return element;\n }\n element = element.parentNode;\n }\n}\n\nmodule.exports = closest;\n\n\n/***/ }),\n\n/***/ 438:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar closest = __webpack_require__(828);\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction _delegate(element, selector, type, callback, useCapture) {\n var listenerFn = listener.apply(this, arguments);\n\n element.addEventListener(type, listenerFn, useCapture);\n\n return {\n destroy: function() {\n element.removeEventListener(type, listenerFn, useCapture);\n }\n }\n}\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element|String|Array} [elements]\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction delegate(elements, selector, type, callback, useCapture) {\n // Handle the regular Element usage\n if (typeof elements.addEventListener === 'function') {\n return _delegate.apply(null, arguments);\n }\n\n // Handle Element-less usage, it defaults to global delegation\n if (typeof type === 'function') {\n // Use `document` as the first parameter, then apply arguments\n // This is a short way to .unshift `arguments` without running into deoptimizations\n return _delegate.bind(null, document).apply(null, arguments);\n }\n\n // Handle Selector-based usage\n if (typeof elements === 'string') {\n elements = document.querySelectorAll(elements);\n }\n\n // Handle Array-like based usage\n return Array.prototype.map.call(elements, function (element) {\n return _delegate(element, selector, type, callback, useCapture);\n });\n}\n\n/**\n * Finds closest match and invokes callback.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Function}\n */\nfunction listener(element, selector, type, callback) {\n return function(e) {\n e.delegateTarget = closest(e.target, selector);\n\n if (e.delegateTarget) {\n callback.call(element, e);\n }\n }\n}\n\nmodule.exports = delegate;\n\n\n/***/ }),\n\n/***/ 879:\n/***/ (function(__unused_webpack_module, exports) {\n\n/**\n * Check if argument is a HTML element.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.node = function(value) {\n return value !== undefined\n && value instanceof HTMLElement\n && value.nodeType === 1;\n};\n\n/**\n * Check if argument is a list of HTML elements.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.nodeList = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return value !== undefined\n && (type === '[object NodeList]' || type === '[object HTMLCollection]')\n && ('length' in value)\n && (value.length === 0 || exports.node(value[0]));\n};\n\n/**\n * Check if argument is a string.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.string = function(value) {\n return typeof value === 'string'\n || value instanceof String;\n};\n\n/**\n * Check if argument is a function.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.fn = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return type === '[object Function]';\n};\n\n\n/***/ }),\n\n/***/ 370:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar is = __webpack_require__(879);\nvar delegate = __webpack_require__(438);\n\n/**\n * Validates all params and calls the right\n * listener function based on its target type.\n *\n * @param {String|HTMLElement|HTMLCollection|NodeList} target\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listen(target, type, callback) {\n if (!target && !type && !callback) {\n throw new Error('Missing required arguments');\n }\n\n if (!is.string(type)) {\n throw new TypeError('Second argument must be a String');\n }\n\n if (!is.fn(callback)) {\n throw new TypeError('Third argument must be a Function');\n }\n\n if (is.node(target)) {\n return listenNode(target, type, callback);\n }\n else if (is.nodeList(target)) {\n return listenNodeList(target, type, callback);\n }\n else if (is.string(target)) {\n return listenSelector(target, type, callback);\n }\n else {\n throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');\n }\n}\n\n/**\n * Adds an event listener to a HTML element\n * and returns a remove listener function.\n *\n * @param {HTMLElement} node\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNode(node, type, callback) {\n node.addEventListener(type, callback);\n\n return {\n destroy: function() {\n node.removeEventListener(type, callback);\n }\n }\n}\n\n/**\n * Add an event listener to a list of HTML elements\n * and returns a remove listener function.\n *\n * @param {NodeList|HTMLCollection} nodeList\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNodeList(nodeList, type, callback) {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.addEventListener(type, callback);\n });\n\n return {\n destroy: function() {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.removeEventListener(type, callback);\n });\n }\n }\n}\n\n/**\n * Add an event listener to a selector\n * and returns a remove listener function.\n *\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenSelector(selector, type, callback) {\n return delegate(document.body, selector, type, callback);\n}\n\nmodule.exports = listen;\n\n\n/***/ }),\n\n/***/ 817:\n/***/ (function(module) {\n\nfunction select(element) {\n var selectedText;\n\n if (element.nodeName === 'SELECT') {\n element.focus();\n\n selectedText = element.value;\n }\n else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {\n var isReadOnly = element.hasAttribute('readonly');\n\n if (!isReadOnly) {\n element.setAttribute('readonly', '');\n }\n\n element.select();\n element.setSelectionRange(0, element.value.length);\n\n if (!isReadOnly) {\n element.removeAttribute('readonly');\n }\n\n selectedText = element.value;\n }\n else {\n if (element.hasAttribute('contenteditable')) {\n element.focus();\n }\n\n var selection = window.getSelection();\n var range = document.createRange();\n\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n\n selectedText = selection.toString();\n }\n\n return selectedText;\n}\n\nmodule.exports = select;\n\n\n/***/ }),\n\n/***/ 279:\n/***/ (function(module) {\n\nfunction E () {\n // Keep this empty so it's easier to inherit from\n // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)\n}\n\nE.prototype = {\n on: function (name, callback, ctx) {\n var e = this.e || (this.e = {});\n\n (e[name] || (e[name] = [])).push({\n fn: callback,\n ctx: ctx\n });\n\n return this;\n },\n\n once: function (name, callback, ctx) {\n var self = this;\n function listener () {\n self.off(name, listener);\n callback.apply(ctx, arguments);\n };\n\n listener._ = callback\n return this.on(name, listener, ctx);\n },\n\n emit: function (name) {\n var data = [].slice.call(arguments, 1);\n var evtArr = ((this.e || (this.e = {}))[name] || []).slice();\n var i = 0;\n var len = evtArr.length;\n\n for (i; i < len; i++) {\n evtArr[i].fn.apply(evtArr[i].ctx, data);\n }\n\n return this;\n },\n\n off: function (name, callback) {\n var e = this.e || (this.e = {});\n var evts = e[name];\n var liveEvents = [];\n\n if (evts && callback) {\n for (var i = 0, len = evts.length; i < len; i++) {\n if (evts[i].fn !== callback && evts[i].fn._ !== callback)\n liveEvents.push(evts[i]);\n }\n }\n\n // Remove event from queue to prevent memory leak\n // Suggested by https://github.com/lazd\n // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910\n\n (liveEvents.length)\n ? e[name] = liveEvents\n : delete e[name];\n\n return this;\n }\n};\n\nmodule.exports = E;\nmodule.exports.TinyEmitter = E;\n\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/ \t\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(__webpack_module_cache__[moduleId]) {\n/******/ \t\t\treturn __webpack_module_cache__[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/ \t\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/ \t\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/ \t\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t!function() {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = function(module) {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\tfunction() { return module['default']; } :\n/******/ \t\t\t\tfunction() { return module; };\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t!function() {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = function(exports, definition) {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t!function() {\n/******/ \t\t__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }\n/******/ \t}();\n/******/ \t\n/************************************************************************/\n/******/ \t// module exports must be returned from runtime so entry inlining is disabled\n/******/ \t// startup\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(686);\n/******/ })()\n.default;\n});", "/*\n * Copyright (c) 2016-2025 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport \"focus-visible\"\n\nimport {\n EMPTY,\n NEVER,\n Observable,\n Subject,\n defer,\n delay,\n filter,\n map,\n merge,\n mergeWith,\n shareReplay,\n switchMap\n} from \"rxjs\"\n\nimport { configuration, feature } from \"./_\"\nimport {\n at,\n getActiveElement,\n getOptionalElement,\n requestJSON,\n setLocation,\n setToggle,\n watchDocument,\n watchKeyboard,\n watchLocation,\n watchLocationTarget,\n watchMedia,\n watchPrint,\n watchScript,\n watchViewport\n} from \"./browser\"\nimport {\n getComponentElement,\n getComponentElements,\n mountAnnounce,\n mountBackToTop,\n mountConsent,\n mountContent,\n mountDialog,\n mountHeader,\n mountHeaderTitle,\n mountPalette,\n mountProgress,\n mountSearch,\n mountSearchHiglight,\n mountSidebar,\n mountSource,\n mountTableOfContents,\n mountTabs,\n watchHeader,\n watchMain\n} from \"./components\"\nimport {\n SearchIndex,\n fetchSitemap,\n setupAlternate,\n setupClipboardJS,\n setupInstantNavigation,\n setupVersionSelector\n} from \"./integrations\"\nimport {\n patchEllipsis,\n patchIndeterminate,\n patchScrollfix,\n patchScrolllock\n} from \"./patches\"\nimport \"./polyfills\"\n\n/* ----------------------------------------------------------------------------\n * Functions - @todo refactor\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch search index\n *\n * @returns Search index observable\n */\nfunction fetchSearchIndex(): Observable {\n if (location.protocol === \"file:\") {\n return watchScript(\n `${new URL(\"search/search_index.js\", config.base)}`\n )\n .pipe(\n // @ts-ignore - @todo fix typings\n map(() => __index),\n shareReplay(1)\n )\n } else {\n return requestJSON(\n new URL(\"search/search_index.json\", config.base)\n )\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Application\n * ------------------------------------------------------------------------- */\n\n/* Yay, JavaScript is available */\ndocument.documentElement.classList.remove(\"no-js\")\ndocument.documentElement.classList.add(\"js\")\n\n/* Set up navigation observables and subjects */\nconst document$ = watchDocument()\nconst location$ = watchLocation()\nconst target$ = watchLocationTarget(location$)\nconst keyboard$ = watchKeyboard()\n\n/* Set up media observables */\nconst viewport$ = watchViewport()\nconst tablet$ = watchMedia(\"(min-width: 60em)\")\nconst screen$ = watchMedia(\"(min-width: 76.25em)\")\nconst print$ = watchPrint()\n\n/* Retrieve search index, if search is enabled */\nconst config = configuration()\nconst index$ = document.forms.namedItem(\"search\")\n ? fetchSearchIndex()\n : NEVER\n\n/* Set up Clipboard.js integration */\nconst alert$ = new Subject()\nsetupClipboardJS({ alert$ })\n\n/* Set up language selector */\nsetupAlternate({ document$ })\n\n/* Set up progress indicator */\nconst progress$ = new Subject()\n\n/* Set up sitemap for instant navigation and previews */\nconst sitemap$ = fetchSitemap(config.base)\n\n/* Set up instant navigation, if enabled */\nif (feature(\"navigation.instant\"))\n setupInstantNavigation({ sitemap$, location$, viewport$, progress$ })\n .subscribe(document$)\n\n/* Set up version selector */\nif (config.version?.provider === \"mike\")\n setupVersionSelector({ document$ })\n\n/* Always close drawer and search on navigation */\nmerge(location$, target$)\n .pipe(\n delay(125)\n )\n .subscribe(() => {\n setToggle(\"drawer\", false)\n setToggle(\"search\", false)\n })\n\n/* Set up global keyboard handlers */\nkeyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\")\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Go to previous page */\n case \"p\":\n case \",\":\n const prev = getOptionalElement(\"link[rel=prev]\")\n if (typeof prev !== \"undefined\")\n setLocation(prev)\n break\n\n /* Go to next page */\n case \"n\":\n case \".\":\n const next = getOptionalElement(\"link[rel=next]\")\n if (typeof next !== \"undefined\")\n setLocation(next)\n break\n\n /* Expand navigation, see https://bit.ly/3ZjG5io */\n case \"Enter\":\n const active = getActiveElement()\n if (active instanceof HTMLLabelElement)\n active.click()\n }\n })\n\n/* Set up patches */\npatchEllipsis({ viewport$, document$ })\npatchIndeterminate({ document$, tablet$ })\npatchScrollfix({ document$ })\npatchScrolllock({ viewport$, tablet$ })\n\n/* Set up header and main area observable */\nconst header$ = watchHeader(getComponentElement(\"header\"), { viewport$ })\nconst main$ = document$\n .pipe(\n map(() => getComponentElement(\"main\")),\n switchMap(el => watchMain(el, { viewport$, header$ })),\n shareReplay(1)\n )\n\n/* Set up control component observables */\nconst control$ = merge(\n\n /* Consent */\n ...getComponentElements(\"consent\")\n .map(el => mountConsent(el, { target$ })),\n\n /* Dialog */\n ...getComponentElements(\"dialog\")\n .map(el => mountDialog(el, { alert$ })),\n\n /* Color palette */\n ...getComponentElements(\"palette\")\n .map(el => mountPalette(el)),\n\n /* Progress bar */\n ...getComponentElements(\"progress\")\n .map(el => mountProgress(el, { progress$ })),\n\n /* Search */\n ...getComponentElements(\"search\")\n .map(el => mountSearch(el, { index$, keyboard$ })),\n\n /* Repository information */\n ...getComponentElements(\"source\")\n .map(el => mountSource(el))\n)\n\n/* Set up content component observables */\nconst content$ = defer(() => merge(\n\n /* Announcement bar */\n ...getComponentElements(\"announce\")\n .map(el => mountAnnounce(el)),\n\n /* Content */\n ...getComponentElements(\"content\")\n .map(el => mountContent(el, { sitemap$, viewport$, target$, print$ })),\n\n /* Search highlighting */\n ...getComponentElements(\"content\")\n .map(el => feature(\"search.highlight\")\n ? mountSearchHiglight(el, { index$, location$ })\n : EMPTY\n ),\n\n /* Header */\n ...getComponentElements(\"header\")\n .map(el => mountHeader(el, { viewport$, header$, main$ })),\n\n /* Header title */\n ...getComponentElements(\"header-title\")\n .map(el => mountHeaderTitle(el, { viewport$, header$ })),\n\n /* Sidebar */\n ...getComponentElements(\"sidebar\")\n .map(el => el.getAttribute(\"data-md-type\") === \"navigation\"\n ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))\n : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))\n ),\n\n /* Navigation tabs */\n ...getComponentElements(\"tabs\")\n .map(el => mountTabs(el, { viewport$, header$ })),\n\n /* Table of contents */\n ...getComponentElements(\"toc\")\n .map(el => mountTableOfContents(el, {\n viewport$, header$, main$, target$\n })),\n\n /* Back-to-top button */\n ...getComponentElements(\"top\")\n .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))\n))\n\n/* Set up component observables */\nconst component$ = document$\n .pipe(\n switchMap(() => content$),\n mergeWith(control$),\n shareReplay(1)\n )\n\n/* Subscribe to all components */\ncomponent$.subscribe()\n\n/* ----------------------------------------------------------------------------\n * Exports\n * ------------------------------------------------------------------------- */\n\nwindow.document$ = document$ /* Document observable */\nwindow.location$ = location$ /* Location subject */\nwindow.target$ = target$ /* Location target observable */\nwindow.keyboard$ = keyboard$ /* Keyboard observable */\nwindow.viewport$ = viewport$ /* Viewport observable */\nwindow.tablet$ = tablet$ /* Media tablet observable */\nwindow.screen$ = screen$ /* Media screen observable */\nwindow.print$ = print$ /* Media print observable */\nwindow.alert$ = alert$ /* Alert subject */\nwindow.progress$ = progress$ /* Progress indicator subject */\nwindow.component$ = component$ /* Component observable */\n", "/******************************************************************************\nCopyright (c) Microsoft Corporation.\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\nPERFORMANCE OF THIS SOFTWARE.\n***************************************************************************** */\n/* global Reflect, Promise, SuppressedError, Symbol, Iterator */\n\nvar extendStatics = function(d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n};\n\nexport function __extends(d, b) {\n if (typeof b !== \"function\" && b !== null)\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n}\n\nexport var __assign = function() {\n __assign = Object.assign || function __assign(t) {\n for (var s, i = 1, n = arguments.length; i < n; i++) {\n s = arguments[i];\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\n }\n return t;\n }\n return __assign.apply(this, arguments);\n}\n\nexport function __rest(s, e) {\n var t = {};\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\n t[p] = s[p];\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\n t[p[i]] = s[p[i]];\n }\n return t;\n}\n\nexport function __decorate(decorators, target, key, desc) {\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\n return c > 3 && r && Object.defineProperty(target, key, r), r;\n}\n\nexport function __param(paramIndex, decorator) {\n return function (target, key) { decorator(target, key, paramIndex); }\n}\n\nexport function __esDecorate(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {\n function accept(f) { if (f !== void 0 && typeof f !== \"function\") throw new TypeError(\"Function expected\"); return f; }\n var kind = contextIn.kind, key = kind === \"getter\" ? \"get\" : kind === \"setter\" ? \"set\" : \"value\";\n var target = !descriptorIn && ctor ? contextIn[\"static\"] ? ctor : ctor.prototype : null;\n var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});\n var _, done = false;\n for (var i = decorators.length - 1; i >= 0; i--) {\n var context = {};\n for (var p in contextIn) context[p] = p === \"access\" ? {} : contextIn[p];\n for (var p in contextIn.access) context.access[p] = contextIn.access[p];\n context.addInitializer = function (f) { if (done) throw new TypeError(\"Cannot add initializers after decoration has completed\"); extraInitializers.push(accept(f || null)); };\n var result = (0, decorators[i])(kind === \"accessor\" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);\n if (kind === \"accessor\") {\n if (result === void 0) continue;\n if (result === null || typeof result !== \"object\") throw new TypeError(\"Object expected\");\n if (_ = accept(result.get)) descriptor.get = _;\n if (_ = accept(result.set)) descriptor.set = _;\n if (_ = accept(result.init)) initializers.unshift(_);\n }\n else if (_ = accept(result)) {\n if (kind === \"field\") initializers.unshift(_);\n else descriptor[key] = _;\n }\n }\n if (target) Object.defineProperty(target, contextIn.name, descriptor);\n done = true;\n};\n\nexport function __runInitializers(thisArg, initializers, value) {\n var useValue = arguments.length > 2;\n for (var i = 0; i < initializers.length; i++) {\n value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);\n }\n return useValue ? value : void 0;\n};\n\nexport function __propKey(x) {\n return typeof x === \"symbol\" ? x : \"\".concat(x);\n};\n\nexport function __setFunctionName(f, name, prefix) {\n if (typeof name === \"symbol\") name = name.description ? \"[\".concat(name.description, \"]\") : \"\";\n return Object.defineProperty(f, \"name\", { configurable: true, value: prefix ? \"\".concat(prefix, \" \", name) : name });\n};\n\nexport function __metadata(metadataKey, metadataValue) {\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\n}\n\nexport function __awaiter(thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n}\n\nexport function __generator(thisArg, body) {\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === \"function\" ? Iterator : Object).prototype);\n return g.next = verb(0), g[\"throw\"] = verb(1), g[\"return\"] = verb(2), typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\n function verb(n) { return function (v) { return step([n, v]); }; }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0: case 1: t = op; break;\n case 4: _.label++; return { value: op[1], done: false };\n case 5: _.label++; y = op[1]; op = [0]; continue;\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\n if (t[2]) _.ops.pop();\n _.trys.pop(); continue;\n }\n op = body.call(thisArg, _);\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\n }\n}\n\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n var desc = Object.getOwnPropertyDescriptor(m, k);\n if (!desc || (\"get\" in desc ? !m.__esModule : desc.writable || desc.configurable)) {\n desc = { enumerable: true, get: function() { return m[k]; } };\n }\n Object.defineProperty(o, k2, desc);\n}) : (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n o[k2] = m[k];\n});\n\nexport function __exportStar(m, o) {\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\n}\n\nexport function __values(o) {\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\n if (m) return m.call(o);\n if (o && typeof o.length === \"number\") return {\n next: function () {\n if (o && i >= o.length) o = void 0;\n return { value: o && o[i++], done: !o };\n }\n };\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\n}\n\nexport function __read(o, n) {\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\n if (!m) return o;\n var i = m.call(o), r, ar = [], e;\n try {\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\n }\n catch (error) { e = { error: error }; }\n finally {\n try {\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\n }\n finally { if (e) throw e.error; }\n }\n return ar;\n}\n\n/** @deprecated */\nexport function __spread() {\n for (var ar = [], i = 0; i < arguments.length; i++)\n ar = ar.concat(__read(arguments[i]));\n return ar;\n}\n\n/** @deprecated */\nexport function __spreadArrays() {\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\n r[k] = a[j];\n return r;\n}\n\nexport function __spreadArray(to, from, pack) {\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\n if (ar || !(i in from)) {\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\n ar[i] = from[i];\n }\n }\n return to.concat(ar || Array.prototype.slice.call(from));\n}\n\nexport function __await(v) {\n return this instanceof __await ? (this.v = v, this) : new __await(v);\n}\n\nexport function __asyncGenerator(thisArg, _arguments, generator) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\n return i = Object.create((typeof AsyncIterator === \"function\" ? AsyncIterator : Object).prototype), verb(\"next\"), verb(\"throw\"), verb(\"return\", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;\n function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }\n function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\n function fulfill(value) { resume(\"next\", value); }\n function reject(value) { resume(\"throw\", value); }\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\n}\n\nexport function __asyncDelegator(o) {\n var i, p;\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; }\n}\n\nexport function __asyncValues(o) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var m = o[Symbol.asyncIterator], i;\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\n}\n\nexport function __makeTemplateObject(cooked, raw) {\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\n return cooked;\n};\n\nvar __setModuleDefault = Object.create ? (function(o, v) {\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\n}) : function(o, v) {\n o[\"default\"] = v;\n};\n\nexport function __importStar(mod) {\n if (mod && mod.__esModule) return mod;\n var result = {};\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\n __setModuleDefault(result, mod);\n return result;\n}\n\nexport function __importDefault(mod) {\n return (mod && mod.__esModule) ? mod : { default: mod };\n}\n\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n}\n\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n}\n\nexport function __classPrivateFieldIn(state, receiver) {\n if (receiver === null || (typeof receiver !== \"object\" && typeof receiver !== \"function\")) throw new TypeError(\"Cannot use 'in' operator on non-object\");\n return typeof state === \"function\" ? receiver === state : state.has(receiver);\n}\n\nexport function __addDisposableResource(env, value, async) {\n if (value !== null && value !== void 0) {\n if (typeof value !== \"object\" && typeof value !== \"function\") throw new TypeError(\"Object expected.\");\n var dispose, inner;\n if (async) {\n if (!Symbol.asyncDispose) throw new TypeError(\"Symbol.asyncDispose is not defined.\");\n dispose = value[Symbol.asyncDispose];\n }\n if (dispose === void 0) {\n if (!Symbol.dispose) throw new TypeError(\"Symbol.dispose is not defined.\");\n dispose = value[Symbol.dispose];\n if (async) inner = dispose;\n }\n if (typeof dispose !== \"function\") throw new TypeError(\"Object not disposable.\");\n if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };\n env.stack.push({ value: value, dispose: dispose, async: async });\n }\n else if (async) {\n env.stack.push({ async: true });\n }\n return value;\n}\n\nvar _SuppressedError = typeof SuppressedError === \"function\" ? SuppressedError : function (error, suppressed, message) {\n var e = new Error(message);\n return e.name = \"SuppressedError\", e.error = error, e.suppressed = suppressed, e;\n};\n\nexport function __disposeResources(env) {\n function fail(e) {\n env.error = env.hasError ? new _SuppressedError(e, env.error, \"An error was suppressed during disposal.\") : e;\n env.hasError = true;\n }\n var r, s = 0;\n function next() {\n while (r = env.stack.pop()) {\n try {\n if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);\n if (r.dispose) {\n var result = r.dispose.call(r.value);\n if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });\n }\n else s |= 1;\n }\n catch (e) {\n fail(e);\n }\n }\n if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();\n if (env.hasError) throw env.error;\n }\n return next();\n}\n\nexport default {\n __extends,\n __assign,\n __rest,\n __decorate,\n __param,\n __metadata,\n __awaiter,\n __generator,\n __createBinding,\n __exportStar,\n __values,\n __read,\n __spread,\n __spreadArrays,\n __spreadArray,\n __await,\n __asyncGenerator,\n __asyncDelegator,\n __asyncValues,\n __makeTemplateObject,\n __importStar,\n __importDefault,\n __classPrivateFieldGet,\n __classPrivateFieldSet,\n __classPrivateFieldIn,\n __addDisposableResource,\n __disposeResources,\n};\n", "/**\n * Returns true if the object is a function.\n * @param value The value to check\n */\nexport function isFunction(value: any): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "/**\n * Used to create Error subclasses until the community moves away from ES5.\n *\n * This is because compiling from TypeScript down to ES5 has issues with subclassing Errors\n * as well as other built-in types: https://github.com/Microsoft/TypeScript/issues/12123\n *\n * @param createImpl A factory function to create the actual constructor implementation. The returned\n * function should be a named function that calls `_super` internally.\n */\nexport function createErrorClass(createImpl: (_super: any) => any): T {\n const _super = (instance: any) => {\n Error.call(instance);\n instance.stack = new Error().stack;\n };\n\n const ctorFunc = createImpl(_super);\n ctorFunc.prototype = Object.create(Error.prototype);\n ctorFunc.prototype.constructor = ctorFunc;\n return ctorFunc;\n}\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface UnsubscriptionError extends Error {\n readonly errors: any[];\n}\n\nexport interface UnsubscriptionErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (errors: any[]): UnsubscriptionError;\n}\n\n/**\n * An error thrown when one or more errors have occurred during the\n * `unsubscribe` of a {@link Subscription}.\n */\nexport const UnsubscriptionError: UnsubscriptionErrorCtor = createErrorClass(\n (_super) =>\n function UnsubscriptionErrorImpl(this: any, errors: (Error | string)[]) {\n _super(this);\n this.message = errors\n ? `${errors.length} errors occurred during unsubscription:\n${errors.map((err, i) => `${i + 1}) ${err.toString()}`).join('\\n ')}`\n : '';\n this.name = 'UnsubscriptionError';\n this.errors = errors;\n }\n);\n", "/**\n * Removes an item from an array, mutating it.\n * @param arr The array to remove the item from\n * @param item The item to remove\n */\nexport function arrRemove(arr: T[] | undefined | null, item: T) {\n if (arr) {\n const index = arr.indexOf(item);\n 0 <= index && arr.splice(index, 1);\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { UnsubscriptionError } from './util/UnsubscriptionError';\nimport { SubscriptionLike, TeardownLogic, Unsubscribable } from './types';\nimport { arrRemove } from './util/arrRemove';\n\n/**\n * Represents a disposable resource, such as the execution of an Observable. A\n * Subscription has one important method, `unsubscribe`, that takes no argument\n * and just disposes the resource held by the subscription.\n *\n * Additionally, subscriptions may be grouped together through the `add()`\n * method, which will attach a child Subscription to the current Subscription.\n * When a Subscription is unsubscribed, all its children (and its grandchildren)\n * will be unsubscribed as well.\n */\nexport class Subscription implements SubscriptionLike {\n public static EMPTY = (() => {\n const empty = new Subscription();\n empty.closed = true;\n return empty;\n })();\n\n /**\n * A flag to indicate whether this Subscription has already been unsubscribed.\n */\n public closed = false;\n\n private _parentage: Subscription[] | Subscription | null = null;\n\n /**\n * The list of registered finalizers to execute upon unsubscription. Adding and removing from this\n * list occurs in the {@link #add} and {@link #remove} methods.\n */\n private _finalizers: Exclude[] | null = null;\n\n /**\n * @param initialTeardown A function executed first as part of the finalization\n * process that is kicked off when {@link #unsubscribe} is called.\n */\n constructor(private initialTeardown?: () => void) {}\n\n /**\n * Disposes the resources held by the subscription. May, for instance, cancel\n * an ongoing Observable execution or cancel any other type of work that\n * started when the Subscription was created.\n */\n unsubscribe(): void {\n let errors: any[] | undefined;\n\n if (!this.closed) {\n this.closed = true;\n\n // Remove this from it's parents.\n const { _parentage } = this;\n if (_parentage) {\n this._parentage = null;\n if (Array.isArray(_parentage)) {\n for (const parent of _parentage) {\n parent.remove(this);\n }\n } else {\n _parentage.remove(this);\n }\n }\n\n const { initialTeardown: initialFinalizer } = this;\n if (isFunction(initialFinalizer)) {\n try {\n initialFinalizer();\n } catch (e) {\n errors = e instanceof UnsubscriptionError ? e.errors : [e];\n }\n }\n\n const { _finalizers } = this;\n if (_finalizers) {\n this._finalizers = null;\n for (const finalizer of _finalizers) {\n try {\n execFinalizer(finalizer);\n } catch (err) {\n errors = errors ?? [];\n if (err instanceof UnsubscriptionError) {\n errors = [...errors, ...err.errors];\n } else {\n errors.push(err);\n }\n }\n }\n }\n\n if (errors) {\n throw new UnsubscriptionError(errors);\n }\n }\n }\n\n /**\n * Adds a finalizer to this subscription, so that finalization will be unsubscribed/called\n * when this subscription is unsubscribed. If this subscription is already {@link #closed},\n * because it has already been unsubscribed, then whatever finalizer is passed to it\n * will automatically be executed (unless the finalizer itself is also a closed subscription).\n *\n * Closed Subscriptions cannot be added as finalizers to any subscription. Adding a closed\n * subscription to a any subscription will result in no operation. (A noop).\n *\n * Adding a subscription to itself, or adding `null` or `undefined` will not perform any\n * operation at all. (A noop).\n *\n * `Subscription` instances that are added to this instance will automatically remove themselves\n * if they are unsubscribed. Functions and {@link Unsubscribable} objects that you wish to remove\n * will need to be removed manually with {@link #remove}\n *\n * @param teardown The finalization logic to add to this subscription.\n */\n add(teardown: TeardownLogic): void {\n // Only add the finalizer if it's not undefined\n // and don't add a subscription to itself.\n if (teardown && teardown !== this) {\n if (this.closed) {\n // If this subscription is already closed,\n // execute whatever finalizer is handed to it automatically.\n execFinalizer(teardown);\n } else {\n if (teardown instanceof Subscription) {\n // We don't add closed subscriptions, and we don't add the same subscription\n // twice. Subscription unsubscribe is idempotent.\n if (teardown.closed || teardown._hasParent(this)) {\n return;\n }\n teardown._addParent(this);\n }\n (this._finalizers = this._finalizers ?? []).push(teardown);\n }\n }\n }\n\n /**\n * Checks to see if a this subscription already has a particular parent.\n * This will signal that this subscription has already been added to the parent in question.\n * @param parent the parent to check for\n */\n private _hasParent(parent: Subscription) {\n const { _parentage } = this;\n return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));\n }\n\n /**\n * Adds a parent to this subscription so it can be removed from the parent if it\n * unsubscribes on it's own.\n *\n * NOTE: THIS ASSUMES THAT {@link _hasParent} HAS ALREADY BEEN CHECKED.\n * @param parent The parent subscription to add\n */\n private _addParent(parent: Subscription) {\n const { _parentage } = this;\n this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;\n }\n\n /**\n * Called on a child when it is removed via {@link #remove}.\n * @param parent The parent to remove\n */\n private _removeParent(parent: Subscription) {\n const { _parentage } = this;\n if (_parentage === parent) {\n this._parentage = null;\n } else if (Array.isArray(_parentage)) {\n arrRemove(_parentage, parent);\n }\n }\n\n /**\n * Removes a finalizer from this subscription that was previously added with the {@link #add} method.\n *\n * Note that `Subscription` instances, when unsubscribed, will automatically remove themselves\n * from every other `Subscription` they have been added to. This means that using the `remove` method\n * is not a common thing and should be used thoughtfully.\n *\n * If you add the same finalizer instance of a function or an unsubscribable object to a `Subscription` instance\n * more than once, you will need to call `remove` the same number of times to remove all instances.\n *\n * All finalizer instances are removed to free up memory upon unsubscription.\n *\n * @param teardown The finalizer to remove from this subscription\n */\n remove(teardown: Exclude): void {\n const { _finalizers } = this;\n _finalizers && arrRemove(_finalizers, teardown);\n\n if (teardown instanceof Subscription) {\n teardown._removeParent(this);\n }\n }\n}\n\nexport const EMPTY_SUBSCRIPTION = Subscription.EMPTY;\n\nexport function isSubscription(value: any): value is Subscription {\n return (\n value instanceof Subscription ||\n (value && 'closed' in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe))\n );\n}\n\nfunction execFinalizer(finalizer: Unsubscribable | (() => void)) {\n if (isFunction(finalizer)) {\n finalizer();\n } else {\n finalizer.unsubscribe();\n }\n}\n", "import { Subscriber } from './Subscriber';\nimport { ObservableNotification } from './types';\n\n/**\n * The {@link GlobalConfig} object for RxJS. It is used to configure things\n * like how to react on unhandled errors.\n */\nexport const config: GlobalConfig = {\n onUnhandledError: null,\n onStoppedNotification: null,\n Promise: undefined,\n useDeprecatedSynchronousErrorHandling: false,\n useDeprecatedNextContext: false,\n};\n\n/**\n * The global configuration object for RxJS, used to configure things\n * like how to react on unhandled errors. Accessible via {@link config}\n * object.\n */\nexport interface GlobalConfig {\n /**\n * A registration point for unhandled errors from RxJS. These are errors that\n * cannot were not handled by consuming code in the usual subscription path. For\n * example, if you have this configured, and you subscribe to an observable without\n * providing an error handler, errors from that subscription will end up here. This\n * will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onUnhandledError: ((err: any) => void) | null;\n\n /**\n * A registration point for notifications that cannot be sent to subscribers because they\n * have completed, errored or have been explicitly unsubscribed. By default, next, complete\n * and error notifications sent to stopped subscribers are noops. However, sometimes callers\n * might want a different behavior. For example, with sources that attempt to report errors\n * to stopped subscribers, a caller can configure RxJS to throw an unhandled error instead.\n * This will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onStoppedNotification: ((notification: ObservableNotification, subscriber: Subscriber) => void) | null;\n\n /**\n * The promise constructor used by default for {@link Observable#toPromise toPromise} and {@link Observable#forEach forEach}\n * methods.\n *\n * @deprecated As of version 8, RxJS will no longer support this sort of injection of a\n * Promise constructor. If you need a Promise implementation other than native promises,\n * please polyfill/patch Promise as you see appropriate. Will be removed in v8.\n */\n Promise?: PromiseConstructorLike;\n\n /**\n * If true, turns on synchronous error rethrowing, which is a deprecated behavior\n * in v6 and higher. This behavior enables bad patterns like wrapping a subscribe\n * call in a try/catch block. It also enables producer interference, a nasty bug\n * where a multicast can be broken for all observers by a downstream consumer with\n * an unhandled error. DO NOT USE THIS FLAG UNLESS IT'S NEEDED TO BUY TIME\n * FOR MIGRATION REASONS.\n *\n * @deprecated As of version 8, RxJS will no longer support synchronous throwing\n * of unhandled errors. All errors will be thrown on a separate call stack to prevent bad\n * behaviors described above. Will be removed in v8.\n */\n useDeprecatedSynchronousErrorHandling: boolean;\n\n /**\n * If true, enables an as-of-yet undocumented feature from v5: The ability to access\n * `unsubscribe()` via `this` context in `next` functions created in observers passed\n * to `subscribe`.\n *\n * This is being removed because the performance was severely problematic, and it could also cause\n * issues when types other than POJOs are passed to subscribe as subscribers, as they will likely have\n * their `this` context overwritten.\n *\n * @deprecated As of version 8, RxJS will no longer support altering the\n * context of next functions provided as part of an observer to Subscribe. Instead,\n * you will have access to a subscription or a signal or token that will allow you to do things like\n * unsubscribe and test closed status. Will be removed in v8.\n */\n useDeprecatedNextContext: boolean;\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetTimeoutFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearTimeoutFunction = (handle: TimerHandle) => void;\n\ninterface TimeoutProvider {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n delegate:\n | {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n }\n | undefined;\n}\n\nexport const timeoutProvider: TimeoutProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setTimeout(handler: () => void, timeout?: number, ...args) {\n const { delegate } = timeoutProvider;\n if (delegate?.setTimeout) {\n return delegate.setTimeout(handler, timeout, ...args);\n }\n return setTimeout(handler, timeout, ...args);\n },\n clearTimeout(handle) {\n const { delegate } = timeoutProvider;\n return (delegate?.clearTimeout || clearTimeout)(handle as any);\n },\n delegate: undefined,\n};\n", "import { config } from '../config';\nimport { timeoutProvider } from '../scheduler/timeoutProvider';\n\n/**\n * Handles an error on another job either with the user-configured {@link onUnhandledError},\n * or by throwing it on that new job so it can be picked up by `window.onerror`, `process.on('error')`, etc.\n *\n * This should be called whenever there is an error that is out-of-band with the subscription\n * or when an error hits a terminal boundary of the subscription and no error handler was provided.\n *\n * @param err the error to report\n */\nexport function reportUnhandledError(err: any) {\n timeoutProvider.setTimeout(() => {\n const { onUnhandledError } = config;\n if (onUnhandledError) {\n // Execute the user-configured error handler.\n onUnhandledError(err);\n } else {\n // Throw so it is picked up by the runtime's uncaught error mechanism.\n throw err;\n }\n });\n}\n", "/* tslint:disable:no-empty */\nexport function noop() { }\n", "import { CompleteNotification, NextNotification, ErrorNotification } from './types';\n\n/**\n * A completion object optimized for memory use and created to be the\n * same \"shape\" as other notifications in v8.\n * @internal\n */\nexport const COMPLETE_NOTIFICATION = (() => createNotification('C', undefined, undefined) as CompleteNotification)();\n\n/**\n * Internal use only. Creates an optimized error notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function errorNotification(error: any): ErrorNotification {\n return createNotification('E', undefined, error) as any;\n}\n\n/**\n * Internal use only. Creates an optimized next notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function nextNotification(value: T) {\n return createNotification('N', value, undefined) as NextNotification;\n}\n\n/**\n * Ensures that all notifications created internally have the same \"shape\" in v8.\n *\n * TODO: This is only exported to support a crazy legacy test in `groupBy`.\n * @internal\n */\nexport function createNotification(kind: 'N' | 'E' | 'C', value: any, error: any) {\n return {\n kind,\n value,\n error,\n };\n}\n", "import { config } from '../config';\n\nlet context: { errorThrown: boolean; error: any } | null = null;\n\n/**\n * Handles dealing with errors for super-gross mode. Creates a context, in which\n * any synchronously thrown errors will be passed to {@link captureError}. Which\n * will record the error such that it will be rethrown after the call back is complete.\n * TODO: Remove in v8\n * @param cb An immediately executed function.\n */\nexport function errorContext(cb: () => void) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n const isRoot = !context;\n if (isRoot) {\n context = { errorThrown: false, error: null };\n }\n cb();\n if (isRoot) {\n const { errorThrown, error } = context!;\n context = null;\n if (errorThrown) {\n throw error;\n }\n }\n } else {\n // This is the general non-deprecated path for everyone that\n // isn't crazy enough to use super-gross mode (useDeprecatedSynchronousErrorHandling)\n cb();\n }\n}\n\n/**\n * Captures errors only in super-gross mode.\n * @param err the error to capture\n */\nexport function captureError(err: any) {\n if (config.useDeprecatedSynchronousErrorHandling && context) {\n context.errorThrown = true;\n context.error = err;\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { Observer, ObservableNotification } from './types';\nimport { isSubscription, Subscription } from './Subscription';\nimport { config } from './config';\nimport { reportUnhandledError } from './util/reportUnhandledError';\nimport { noop } from './util/noop';\nimport { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories';\nimport { timeoutProvider } from './scheduler/timeoutProvider';\nimport { captureError } from './util/errorContext';\n\n/**\n * Implements the {@link Observer} interface and extends the\n * {@link Subscription} class. While the {@link Observer} is the public API for\n * consuming the values of an {@link Observable}, all Observers get converted to\n * a Subscriber, in order to provide Subscription-like capabilities such as\n * `unsubscribe`. Subscriber is a common type in RxJS, and crucial for\n * implementing operators, but it is rarely used as a public API.\n */\nexport class Subscriber extends Subscription implements Observer {\n /**\n * A static factory for a Subscriber, given a (potentially partial) definition\n * of an Observer.\n * @param next The `next` callback of an Observer.\n * @param error The `error` callback of an\n * Observer.\n * @param complete The `complete` callback of an\n * Observer.\n * @return A Subscriber wrapping the (partially defined)\n * Observer represented by the given arguments.\n * @deprecated Do not use. Will be removed in v8. There is no replacement for this\n * method, and there is no reason to be creating instances of `Subscriber` directly.\n * If you have a specific use case, please file an issue.\n */\n static create(next?: (x?: T) => void, error?: (e?: any) => void, complete?: () => void): Subscriber {\n return new SafeSubscriber(next, error, complete);\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected isStopped: boolean = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected destination: Subscriber | Observer; // this `any` is the escape hatch to erase extra type param (e.g. R)\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * There is no reason to directly create an instance of Subscriber. This type is exported for typings reasons.\n */\n constructor(destination?: Subscriber | Observer) {\n super();\n if (destination) {\n this.destination = destination;\n // Automatically chain subscriptions together here.\n // if destination is a Subscription, then it is a Subscriber.\n if (isSubscription(destination)) {\n destination.add(this);\n }\n } else {\n this.destination = EMPTY_OBSERVER;\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `next` from\n * the Observable, with a value. The Observable may call this method 0 or more\n * times.\n * @param value The `next` value.\n */\n next(value: T): void {\n if (this.isStopped) {\n handleStoppedNotification(nextNotification(value), this);\n } else {\n this._next(value!);\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `error` from\n * the Observable, with an attached `Error`. Notifies the Observer that\n * the Observable has experienced an error condition.\n * @param err The `error` exception.\n */\n error(err?: any): void {\n if (this.isStopped) {\n handleStoppedNotification(errorNotification(err), this);\n } else {\n this.isStopped = true;\n this._error(err);\n }\n }\n\n /**\n * The {@link Observer} callback to receive a valueless notification of type\n * `complete` from the Observable. Notifies the Observer that the Observable\n * has finished sending push-based notifications.\n */\n complete(): void {\n if (this.isStopped) {\n handleStoppedNotification(COMPLETE_NOTIFICATION, this);\n } else {\n this.isStopped = true;\n this._complete();\n }\n }\n\n unsubscribe(): void {\n if (!this.closed) {\n this.isStopped = true;\n super.unsubscribe();\n this.destination = null!;\n }\n }\n\n protected _next(value: T): void {\n this.destination.next(value);\n }\n\n protected _error(err: any): void {\n try {\n this.destination.error(err);\n } finally {\n this.unsubscribe();\n }\n }\n\n protected _complete(): void {\n try {\n this.destination.complete();\n } finally {\n this.unsubscribe();\n }\n }\n}\n\n/**\n * This bind is captured here because we want to be able to have\n * compatibility with monoid libraries that tend to use a method named\n * `bind`. In particular, a library called Monio requires this.\n */\nconst _bind = Function.prototype.bind;\n\nfunction bind any>(fn: Fn, thisArg: any): Fn {\n return _bind.call(fn, thisArg);\n}\n\n/**\n * Internal optimization only, DO NOT EXPOSE.\n * @internal\n */\nclass ConsumerObserver implements Observer {\n constructor(private partialObserver: Partial>) {}\n\n next(value: T): void {\n const { partialObserver } = this;\n if (partialObserver.next) {\n try {\n partialObserver.next(value);\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n\n error(err: any): void {\n const { partialObserver } = this;\n if (partialObserver.error) {\n try {\n partialObserver.error(err);\n } catch (error) {\n handleUnhandledError(error);\n }\n } else {\n handleUnhandledError(err);\n }\n }\n\n complete(): void {\n const { partialObserver } = this;\n if (partialObserver.complete) {\n try {\n partialObserver.complete();\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n}\n\nexport class SafeSubscriber extends Subscriber {\n constructor(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((e?: any) => void) | null,\n complete?: (() => void) | null\n ) {\n super();\n\n let partialObserver: Partial>;\n if (isFunction(observerOrNext) || !observerOrNext) {\n // The first argument is a function, not an observer. The next\n // two arguments *could* be observers, or they could be empty.\n partialObserver = {\n next: (observerOrNext ?? undefined) as ((value: T) => void) | undefined,\n error: error ?? undefined,\n complete: complete ?? undefined,\n };\n } else {\n // The first argument is a partial observer.\n let context: any;\n if (this && config.useDeprecatedNextContext) {\n // This is a deprecated path that made `this.unsubscribe()` available in\n // next handler functions passed to subscribe. This only exists behind a flag\n // now, as it is *very* slow.\n context = Object.create(observerOrNext);\n context.unsubscribe = () => this.unsubscribe();\n partialObserver = {\n next: observerOrNext.next && bind(observerOrNext.next, context),\n error: observerOrNext.error && bind(observerOrNext.error, context),\n complete: observerOrNext.complete && bind(observerOrNext.complete, context),\n };\n } else {\n // The \"normal\" path. Just use the partial observer directly.\n partialObserver = observerOrNext;\n }\n }\n\n // Wrap the partial observer to ensure it's a full observer, and\n // make sure proper error handling is accounted for.\n this.destination = new ConsumerObserver(partialObserver);\n }\n}\n\nfunction handleUnhandledError(error: any) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n captureError(error);\n } else {\n // Ideal path, we report this as an unhandled error,\n // which is thrown on a new call stack.\n reportUnhandledError(error);\n }\n}\n\n/**\n * An error handler used when no error handler was supplied\n * to the SafeSubscriber -- meaning no error handler was supplied\n * do the `subscribe` call on our observable.\n * @param err The error to handle\n */\nfunction defaultErrorHandler(err: any) {\n throw err;\n}\n\n/**\n * A handler for notifications that cannot be sent to a stopped subscriber.\n * @param notification The notification being sent.\n * @param subscriber The stopped subscriber.\n */\nfunction handleStoppedNotification(notification: ObservableNotification, subscriber: Subscriber) {\n const { onStoppedNotification } = config;\n onStoppedNotification && timeoutProvider.setTimeout(() => onStoppedNotification(notification, subscriber));\n}\n\n/**\n * The observer used as a stub for subscriptions where the user did not\n * pass any arguments to `subscribe`. Comes with the default error handling\n * behavior.\n */\nexport const EMPTY_OBSERVER: Readonly> & { closed: true } = {\n closed: true,\n next: noop,\n error: defaultErrorHandler,\n complete: noop,\n};\n", "/**\n * Symbol.observable or a string \"@@observable\". Used for interop\n *\n * @deprecated We will no longer be exporting this symbol in upcoming versions of RxJS.\n * Instead polyfill and use Symbol.observable directly *or* use https://www.npmjs.com/package/symbol-observable\n */\nexport const observable: string | symbol = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')();\n", "/**\n * This function takes one parameter and just returns it. Simply put,\n * this is like `(x: T): T => x`.\n *\n * ## Examples\n *\n * This is useful in some cases when using things like `mergeMap`\n *\n * ```ts\n * import { interval, take, map, range, mergeMap, identity } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(5));\n *\n * const result$ = source$.pipe(\n * map(i => range(i)),\n * mergeMap(identity) // same as mergeMap(x => x)\n * );\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * Or when you want to selectively apply an operator\n *\n * ```ts\n * import { interval, take, identity } from 'rxjs';\n *\n * const shouldLimit = () => Math.random() < 0.5;\n *\n * const source$ = interval(1000);\n *\n * const result$ = source$.pipe(shouldLimit() ? take(5) : identity);\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * @param x Any value that is returned by this function\n * @returns The value passed as the first parameter to this function\n */\nexport function identity(x: T): T {\n return x;\n}\n", "import { identity } from './identity';\nimport { UnaryFunction } from '../types';\n\nexport function pipe(): typeof identity;\nexport function pipe(fn1: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction, fn3: UnaryFunction): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction,\n ...fns: UnaryFunction[]\n): UnaryFunction;\n\n/**\n * pipe() can be called on one or more functions, each of which can take one argument (\"UnaryFunction\")\n * and uses it to return a value.\n * It returns a function that takes one argument, passes it to the first UnaryFunction, and then\n * passes the result to the next one, passes that result to the next one, and so on. \n */\nexport function pipe(...fns: Array>): UnaryFunction {\n return pipeFromArray(fns);\n}\n\n/** @internal */\nexport function pipeFromArray(fns: Array>): UnaryFunction {\n if (fns.length === 0) {\n return identity as UnaryFunction;\n }\n\n if (fns.length === 1) {\n return fns[0];\n }\n\n return function piped(input: T): R {\n return fns.reduce((prev: any, fn: UnaryFunction) => fn(prev), input as any);\n };\n}\n", "import { Operator } from './Operator';\nimport { SafeSubscriber, Subscriber } from './Subscriber';\nimport { isSubscription, Subscription } from './Subscription';\nimport { TeardownLogic, OperatorFunction, Subscribable, Observer } from './types';\nimport { observable as Symbol_observable } from './symbol/observable';\nimport { pipeFromArray } from './util/pipe';\nimport { config } from './config';\nimport { isFunction } from './util/isFunction';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A representation of any set of values over any amount of time. This is the most basic building block\n * of RxJS.\n */\nexport class Observable implements Subscribable {\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n source: Observable | undefined;\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n operator: Operator | undefined;\n\n /**\n * @param subscribe The function that is called when the Observable is\n * initially subscribed to. This function is given a Subscriber, to which new values\n * can be `next`ed, or an `error` method can be called to raise an error, or\n * `complete` can be called to notify of a successful completion.\n */\n constructor(subscribe?: (this: Observable, subscriber: Subscriber) => TeardownLogic) {\n if (subscribe) {\n this._subscribe = subscribe;\n }\n }\n\n // HACK: Since TypeScript inherits static properties too, we have to\n // fight against TypeScript here so Subject can have a different static create signature\n /**\n * Creates a new Observable by calling the Observable constructor\n * @param subscribe the subscriber function to be passed to the Observable constructor\n * @return A new observable.\n * @deprecated Use `new Observable()` instead. Will be removed in v8.\n */\n static create: (...args: any[]) => any = (subscribe?: (subscriber: Subscriber) => TeardownLogic) => {\n return new Observable(subscribe);\n };\n\n /**\n * Creates a new Observable, with this Observable instance as the source, and the passed\n * operator defined as the new observable's operator.\n * @param operator the operator defining the operation to take on the observable\n * @return A new observable with the Operator applied.\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * If you have implemented an operator using `lift`, it is recommended that you create an\n * operator by simply returning `new Observable()` directly. See \"Creating new operators from\n * scratch\" section here: https://rxjs.dev/guide/operators\n */\n lift(operator?: Operator): Observable {\n const observable = new Observable();\n observable.source = this;\n observable.operator = operator;\n return observable;\n }\n\n subscribe(observerOrNext?: Partial> | ((value: T) => void)): Subscription;\n /** @deprecated Instead of passing separate callback arguments, use an observer argument. Signatures taking separate callback arguments will be removed in v8. Details: https://rxjs.dev/deprecations/subscribe-arguments */\n subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription;\n /**\n * Invokes an execution of an Observable and registers Observer handlers for notifications it will emit.\n *\n * Use it when you have all these Observables, but still nothing is happening.\n *\n * `subscribe` is not a regular operator, but a method that calls Observable's internal `subscribe` function. It\n * might be for example a function that you passed to Observable's constructor, but most of the time it is\n * a library implementation, which defines what will be emitted by an Observable, and when it be will emitted. This means\n * that calling `subscribe` is actually the moment when Observable starts its work, not when it is created, as it is often\n * the thought.\n *\n * Apart from starting the execution of an Observable, this method allows you to listen for values\n * that an Observable emits, as well as for when it completes or errors. You can achieve this in two\n * of the following ways.\n *\n * The first way is creating an object that implements {@link Observer} interface. It should have methods\n * defined by that interface, but note that it should be just a regular JavaScript object, which you can create\n * yourself in any way you want (ES6 class, classic function constructor, object literal etc.). In particular, do\n * not attempt to use any RxJS implementation details to create Observers - you don't need them. Remember also\n * that your object does not have to implement all methods. If you find yourself creating a method that doesn't\n * do anything, you can simply omit it. Note however, if the `error` method is not provided and an error happens,\n * it will be thrown asynchronously. Errors thrown asynchronously cannot be caught using `try`/`catch`. Instead,\n * use the {@link onUnhandledError} configuration option or use a runtime handler (like `window.onerror` or\n * `process.on('error)`) to be notified of unhandled errors. Because of this, it's recommended that you provide\n * an `error` method to avoid missing thrown errors.\n *\n * The second way is to give up on Observer object altogether and simply provide callback functions in place of its methods.\n * This means you can provide three functions as arguments to `subscribe`, where the first function is equivalent\n * of a `next` method, the second of an `error` method and the third of a `complete` method. Just as in case of an Observer,\n * if you do not need to listen for something, you can omit a function by passing `undefined` or `null`,\n * since `subscribe` recognizes these functions by where they were placed in function call. When it comes\n * to the `error` function, as with an Observer, if not provided, errors emitted by an Observable will be thrown asynchronously.\n *\n * You can, however, subscribe with no parameters at all. This may be the case where you're not interested in terminal events\n * and you also handled emissions internally by using operators (e.g. using `tap`).\n *\n * Whichever style of calling `subscribe` you use, in both cases it returns a Subscription object.\n * This object allows you to call `unsubscribe` on it, which in turn will stop the work that an Observable does and will clean\n * up all resources that an Observable used. Note that cancelling a subscription will not call `complete` callback\n * provided to `subscribe` function, which is reserved for a regular completion signal that comes from an Observable.\n *\n * Remember that callbacks provided to `subscribe` are not guaranteed to be called asynchronously.\n * It is an Observable itself that decides when these functions will be called. For example {@link of}\n * by default emits all its values synchronously. Always check documentation for how given Observable\n * will behave when subscribed and if its default behavior can be modified with a `scheduler`.\n *\n * #### Examples\n *\n * Subscribe with an {@link guide/observer Observer}\n *\n * ```ts\n * import { of } from 'rxjs';\n *\n * const sumObserver = {\n * sum: 0,\n * next(value) {\n * console.log('Adding: ' + value);\n * this.sum = this.sum + value;\n * },\n * error() {\n * // We actually could just remove this method,\n * // since we do not really care about errors right now.\n * },\n * complete() {\n * console.log('Sum equals: ' + this.sum);\n * }\n * };\n *\n * of(1, 2, 3) // Synchronously emits 1, 2, 3 and then completes.\n * .subscribe(sumObserver);\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Subscribe with functions ({@link deprecations/subscribe-arguments deprecated})\n *\n * ```ts\n * import { of } from 'rxjs'\n *\n * let sum = 0;\n *\n * of(1, 2, 3).subscribe(\n * value => {\n * console.log('Adding: ' + value);\n * sum = sum + value;\n * },\n * undefined,\n * () => console.log('Sum equals: ' + sum)\n * );\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Cancel a subscription\n *\n * ```ts\n * import { interval } from 'rxjs';\n *\n * const subscription = interval(1000).subscribe({\n * next(num) {\n * console.log(num)\n * },\n * complete() {\n * // Will not be called, even when cancelling subscription.\n * console.log('completed!');\n * }\n * });\n *\n * setTimeout(() => {\n * subscription.unsubscribe();\n * console.log('unsubscribed!');\n * }, 2500);\n *\n * // Logs:\n * // 0 after 1s\n * // 1 after 2s\n * // 'unsubscribed!' after 2.5s\n * ```\n *\n * @param observerOrNext Either an {@link Observer} with some or all callback methods,\n * or the `next` handler that is called for each value emitted from the subscribed Observable.\n * @param error A handler for a terminal event resulting from an error. If no error handler is provided,\n * the error will be thrown asynchronously as unhandled.\n * @param complete A handler for a terminal event resulting from successful completion.\n * @return A subscription reference to the registered handlers.\n */\n subscribe(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((error: any) => void) | null,\n complete?: (() => void) | null\n ): Subscription {\n const subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);\n\n errorContext(() => {\n const { operator, source } = this;\n subscriber.add(\n operator\n ? // We're dealing with a subscription in the\n // operator chain to one of our lifted operators.\n operator.call(subscriber, source)\n : source\n ? // If `source` has a value, but `operator` does not, something that\n // had intimate knowledge of our API, like our `Subject`, must have\n // set it. We're going to just call `_subscribe` directly.\n this._subscribe(subscriber)\n : // In all other cases, we're likely wrapping a user-provided initializer\n // function, so we need to catch errors and handle them appropriately.\n this._trySubscribe(subscriber)\n );\n });\n\n return subscriber;\n }\n\n /** @internal */\n protected _trySubscribe(sink: Subscriber): TeardownLogic {\n try {\n return this._subscribe(sink);\n } catch (err) {\n // We don't need to return anything in this case,\n // because it's just going to try to `add()` to a subscription\n // above.\n sink.error(err);\n }\n }\n\n /**\n * Used as a NON-CANCELLABLE means of subscribing to an observable, for use with\n * APIs that expect promises, like `async/await`. You cannot unsubscribe from this.\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * #### Example\n *\n * ```ts\n * import { interval, take } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(4));\n *\n * async function getTotal() {\n * let total = 0;\n *\n * await source$.forEach(value => {\n * total += value;\n * console.log('observable -> ' + value);\n * });\n *\n * return total;\n * }\n *\n * getTotal().then(\n * total => console.log('Total: ' + total)\n * );\n *\n * // Expected:\n * // 'observable -> 0'\n * // 'observable -> 1'\n * // 'observable -> 2'\n * // 'observable -> 3'\n * // 'Total: 6'\n * ```\n *\n * @param next A handler for each value emitted by the observable.\n * @return A promise that either resolves on observable completion or\n * rejects with the handled error.\n */\n forEach(next: (value: T) => void): Promise;\n\n /**\n * @param next a handler for each value emitted by the observable\n * @param promiseCtor a constructor function used to instantiate the Promise\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n * @deprecated Passing a Promise constructor will no longer be available\n * in upcoming versions of RxJS. This is because it adds weight to the library, for very\n * little benefit. If you need this functionality, it is recommended that you either\n * polyfill Promise, or you create an adapter to convert the returned native promise\n * to whatever promise implementation you wanted. Will be removed in v8.\n */\n forEach(next: (value: T) => void, promiseCtor: PromiseConstructorLike): Promise;\n\n forEach(next: (value: T) => void, promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n const subscriber = new SafeSubscriber({\n next: (value) => {\n try {\n next(value);\n } catch (err) {\n reject(err);\n subscriber.unsubscribe();\n }\n },\n error: reject,\n complete: resolve,\n });\n this.subscribe(subscriber);\n }) as Promise;\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): TeardownLogic {\n return this.source?.subscribe(subscriber);\n }\n\n /**\n * An interop point defined by the es7-observable spec https://github.com/zenparsing/es-observable\n * @return This instance of the observable.\n */\n [Symbol_observable]() {\n return this;\n }\n\n /* tslint:disable:max-line-length */\n pipe(): Observable;\n pipe(op1: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction,\n ...operations: OperatorFunction[]\n ): Observable;\n /* tslint:enable:max-line-length */\n\n /**\n * Used to stitch together functional operators into a chain.\n *\n * ## Example\n *\n * ```ts\n * import { interval, filter, map, scan } from 'rxjs';\n *\n * interval(1000)\n * .pipe(\n * filter(x => x % 2 === 0),\n * map(x => x + x),\n * scan((acc, x) => acc + x)\n * )\n * .subscribe(x => console.log(x));\n * ```\n *\n * @return The Observable result of all the operators having been called\n * in the order they were passed in.\n */\n pipe(...operations: OperatorFunction[]): Observable {\n return pipeFromArray(operations)(this);\n }\n\n /* tslint:disable:max-line-length */\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: typeof Promise): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: PromiseConstructorLike): Promise;\n /* tslint:enable:max-line-length */\n\n /**\n * Subscribe to this Observable and get a Promise resolving on\n * `complete` with the last emission (if any).\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * @param [promiseCtor] a constructor function used to instantiate\n * the Promise\n * @return A Promise that resolves with the last value emit, or\n * rejects on an error. If there were no emissions, Promise\n * resolves with undefined.\n * @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise\n */\n toPromise(promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n let value: T | undefined;\n this.subscribe(\n (x: T) => (value = x),\n (err: any) => reject(err),\n () => resolve(value)\n );\n }) as Promise;\n }\n}\n\n/**\n * Decides between a passed promise constructor from consuming code,\n * A default configured promise constructor, and the native promise\n * constructor and returns it. If nothing can be found, it will throw\n * an error.\n * @param promiseCtor The optional promise constructor to passed by consuming code\n */\nfunction getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) {\n return promiseCtor ?? config.Promise ?? Promise;\n}\n\nfunction isObserver(value: any): value is Observer {\n return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete);\n}\n\nfunction isSubscriber(value: any): value is Subscriber {\n return (value && value instanceof Subscriber) || (isObserver(value) && isSubscription(value));\n}\n", "import { Observable } from '../Observable';\nimport { Subscriber } from '../Subscriber';\nimport { OperatorFunction } from '../types';\nimport { isFunction } from './isFunction';\n\n/**\n * Used to determine if an object is an Observable with a lift function.\n */\nexport function hasLift(source: any): source is { lift: InstanceType['lift'] } {\n return isFunction(source?.lift);\n}\n\n/**\n * Creates an `OperatorFunction`. Used to define operators throughout the library in a concise way.\n * @param init The logic to connect the liftedSource to the subscriber at the moment of subscription.\n */\nexport function operate(\n init: (liftedSource: Observable, subscriber: Subscriber) => (() => void) | void\n): OperatorFunction {\n return (source: Observable) => {\n if (hasLift(source)) {\n return source.lift(function (this: Subscriber, liftedSource: Observable) {\n try {\n return init(liftedSource, this);\n } catch (err) {\n this.error(err);\n }\n });\n }\n throw new TypeError('Unable to lift unknown Observable type');\n };\n}\n", "import { Subscriber } from '../Subscriber';\n\n/**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional teardown logic here. This will only be called on teardown if the\n * subscriber itself is not already closed. This is called after all other teardown logic is executed.\n */\nexport function createOperatorSubscriber(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n onFinalize?: () => void\n): Subscriber {\n return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize);\n}\n\n/**\n * A generic helper for allowing operators to be created with a Subscriber and\n * use closures to capture necessary state from the operator function itself.\n */\nexport class OperatorSubscriber extends Subscriber {\n /**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional finalization logic here. This will only be called on finalization if the\n * subscriber itself is not already closed. This is called after all other finalization logic is executed.\n * @param shouldUnsubscribe An optional check to see if an unsubscribe call should truly unsubscribe.\n * NOTE: This currently **ONLY** exists to support the strange behavior of {@link groupBy}, where unsubscription\n * to the resulting observable does not actually disconnect from the source if there are active subscriptions\n * to any grouped observable. (DO NOT EXPOSE OR USE EXTERNALLY!!!)\n */\n constructor(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n private onFinalize?: () => void,\n private shouldUnsubscribe?: () => boolean\n ) {\n // It's important - for performance reasons - that all of this class's\n // members are initialized and that they are always initialized in the same\n // order. This will ensure that all OperatorSubscriber instances have the\n // same hidden class in V8. This, in turn, will help keep the number of\n // hidden classes involved in property accesses within the base class as\n // low as possible. If the number of hidden classes involved exceeds four,\n // the property accesses will become megamorphic and performance penalties\n // will be incurred - i.e. inline caches won't be used.\n //\n // The reasons for ensuring all instances have the same hidden class are\n // further discussed in this blog post from Benedikt Meurer:\n // https://benediktmeurer.de/2018/03/23/impact-of-polymorphism-on-component-based-frameworks-like-react/\n super(destination);\n this._next = onNext\n ? function (this: OperatorSubscriber, value: T) {\n try {\n onNext(value);\n } catch (err) {\n destination.error(err);\n }\n }\n : super._next;\n this._error = onError\n ? function (this: OperatorSubscriber, err: any) {\n try {\n onError(err);\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._error;\n this._complete = onComplete\n ? function (this: OperatorSubscriber) {\n try {\n onComplete();\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._complete;\n }\n\n unsubscribe() {\n if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) {\n const { closed } = this;\n super.unsubscribe();\n // Execute additional teardown if we have any and we didn't already do so.\n !closed && this.onFinalize?.();\n }\n }\n}\n", "import { Subscription } from '../Subscription';\n\ninterface AnimationFrameProvider {\n schedule(callback: FrameRequestCallback): Subscription;\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n delegate:\n | {\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n }\n | undefined;\n}\n\nexport const animationFrameProvider: AnimationFrameProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n schedule(callback) {\n let request = requestAnimationFrame;\n let cancel: typeof cancelAnimationFrame | undefined = cancelAnimationFrame;\n const { delegate } = animationFrameProvider;\n if (delegate) {\n request = delegate.requestAnimationFrame;\n cancel = delegate.cancelAnimationFrame;\n }\n const handle = request((timestamp) => {\n // Clear the cancel function. The request has been fulfilled, so\n // attempting to cancel the request upon unsubscription would be\n // pointless.\n cancel = undefined;\n callback(timestamp);\n });\n return new Subscription(() => cancel?.(handle));\n },\n requestAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.requestAnimationFrame || requestAnimationFrame)(...args);\n },\n cancelAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.cancelAnimationFrame || cancelAnimationFrame)(...args);\n },\n delegate: undefined,\n};\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface ObjectUnsubscribedError extends Error {}\n\nexport interface ObjectUnsubscribedErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (): ObjectUnsubscribedError;\n}\n\n/**\n * An error thrown when an action is invalid because the object has been\n * unsubscribed.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n *\n * @class ObjectUnsubscribedError\n */\nexport const ObjectUnsubscribedError: ObjectUnsubscribedErrorCtor = createErrorClass(\n (_super) =>\n function ObjectUnsubscribedErrorImpl(this: any) {\n _super(this);\n this.name = 'ObjectUnsubscribedError';\n this.message = 'object unsubscribed';\n }\n);\n", "import { Operator } from './Operator';\nimport { Observable } from './Observable';\nimport { Subscriber } from './Subscriber';\nimport { Subscription, EMPTY_SUBSCRIPTION } from './Subscription';\nimport { Observer, SubscriptionLike, TeardownLogic } from './types';\nimport { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError';\nimport { arrRemove } from './util/arrRemove';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A Subject is a special type of Observable that allows values to be\n * multicasted to many Observers. Subjects are like EventEmitters.\n *\n * Every Subject is an Observable and an Observer. You can subscribe to a\n * Subject, and you can call next to feed values as well as error and complete.\n */\nexport class Subject extends Observable implements SubscriptionLike {\n closed = false;\n\n private currentObservers: Observer[] | null = null;\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n observers: Observer[] = [];\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n isStopped = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n hasError = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n thrownError: any = null;\n\n /**\n * Creates a \"subject\" by basically gluing an observer to an observable.\n *\n * @deprecated Recommended you do not use. Will be removed at some point in the future. Plans for replacement still under discussion.\n */\n static create: (...args: any[]) => any = (destination: Observer, source: Observable): AnonymousSubject => {\n return new AnonymousSubject(destination, source);\n };\n\n constructor() {\n // NOTE: This must be here to obscure Observable's constructor.\n super();\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n lift(operator: Operator): Observable {\n const subject = new AnonymousSubject(this, this);\n subject.operator = operator as any;\n return subject as any;\n }\n\n /** @internal */\n protected _throwIfClosed() {\n if (this.closed) {\n throw new ObjectUnsubscribedError();\n }\n }\n\n next(value: T) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n if (!this.currentObservers) {\n this.currentObservers = Array.from(this.observers);\n }\n for (const observer of this.currentObservers) {\n observer.next(value);\n }\n }\n });\n }\n\n error(err: any) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.hasError = this.isStopped = true;\n this.thrownError = err;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.error(err);\n }\n }\n });\n }\n\n complete() {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.isStopped = true;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.complete();\n }\n }\n });\n }\n\n unsubscribe() {\n this.isStopped = this.closed = true;\n this.observers = this.currentObservers = null!;\n }\n\n get observed() {\n return this.observers?.length > 0;\n }\n\n /** @internal */\n protected _trySubscribe(subscriber: Subscriber): TeardownLogic {\n this._throwIfClosed();\n return super._trySubscribe(subscriber);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._checkFinalizedStatuses(subscriber);\n return this._innerSubscribe(subscriber);\n }\n\n /** @internal */\n protected _innerSubscribe(subscriber: Subscriber) {\n const { hasError, isStopped, observers } = this;\n if (hasError || isStopped) {\n return EMPTY_SUBSCRIPTION;\n }\n this.currentObservers = null;\n observers.push(subscriber);\n return new Subscription(() => {\n this.currentObservers = null;\n arrRemove(observers, subscriber);\n });\n }\n\n /** @internal */\n protected _checkFinalizedStatuses(subscriber: Subscriber) {\n const { hasError, thrownError, isStopped } = this;\n if (hasError) {\n subscriber.error(thrownError);\n } else if (isStopped) {\n subscriber.complete();\n }\n }\n\n /**\n * Creates a new Observable with this Subject as the source. You can do this\n * to create custom Observer-side logic of the Subject and conceal it from\n * code that uses the Observable.\n * @return Observable that this Subject casts to.\n */\n asObservable(): Observable {\n const observable: any = new Observable();\n observable.source = this;\n return observable;\n }\n}\n\nexport class AnonymousSubject extends Subject {\n constructor(\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n public destination?: Observer,\n source?: Observable\n ) {\n super();\n this.source = source;\n }\n\n next(value: T) {\n this.destination?.next?.(value);\n }\n\n error(err: any) {\n this.destination?.error?.(err);\n }\n\n complete() {\n this.destination?.complete?.();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n return this.source?.subscribe(subscriber) ?? EMPTY_SUBSCRIPTION;\n }\n}\n", "import { Subject } from './Subject';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\n\n/**\n * A variant of Subject that requires an initial value and emits its current\n * value whenever it is subscribed to.\n */\nexport class BehaviorSubject extends Subject {\n constructor(private _value: T) {\n super();\n }\n\n get value(): T {\n return this.getValue();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n const subscription = super._subscribe(subscriber);\n !subscription.closed && subscriber.next(this._value);\n return subscription;\n }\n\n getValue(): T {\n const { hasError, thrownError, _value } = this;\n if (hasError) {\n throw thrownError;\n }\n this._throwIfClosed();\n return _value;\n }\n\n next(value: T): void {\n super.next((this._value = value));\n }\n}\n", "import { TimestampProvider } from '../types';\n\ninterface DateTimestampProvider extends TimestampProvider {\n delegate: TimestampProvider | undefined;\n}\n\nexport const dateTimestampProvider: DateTimestampProvider = {\n now() {\n // Use the variable rather than `this` so that the function can be called\n // without being bound to the provider.\n return (dateTimestampProvider.delegate || Date).now();\n },\n delegate: undefined,\n};\n", "import { Subject } from './Subject';\nimport { TimestampProvider } from './types';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * A variant of {@link Subject} that \"replays\" old values to new subscribers by emitting them when they first subscribe.\n *\n * `ReplaySubject` has an internal buffer that will store a specified number of values that it has observed. Like `Subject`,\n * `ReplaySubject` \"observes\" values by having them passed to its `next` method. When it observes a value, it will store that\n * value for a time determined by the configuration of the `ReplaySubject`, as passed to its constructor.\n *\n * When a new subscriber subscribes to the `ReplaySubject` instance, it will synchronously emit all values in its buffer in\n * a First-In-First-Out (FIFO) manner. The `ReplaySubject` will also complete, if it has observed completion; and it will\n * error if it has observed an error.\n *\n * There are two main configuration items to be concerned with:\n *\n * 1. `bufferSize` - This will determine how many items are stored in the buffer, defaults to infinite.\n * 2. `windowTime` - The amount of time to hold a value in the buffer before removing it from the buffer.\n *\n * Both configurations may exist simultaneously. So if you would like to buffer a maximum of 3 values, as long as the values\n * are less than 2 seconds old, you could do so with a `new ReplaySubject(3, 2000)`.\n *\n * ### Differences with BehaviorSubject\n *\n * `BehaviorSubject` is similar to `new ReplaySubject(1)`, with a couple of exceptions:\n *\n * 1. `BehaviorSubject` comes \"primed\" with a single value upon construction.\n * 2. `ReplaySubject` will replay values, even after observing an error, where `BehaviorSubject` will not.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n * @see {@link shareReplay}\n */\nexport class ReplaySubject extends Subject {\n private _buffer: (T | number)[] = [];\n private _infiniteTimeWindow = true;\n\n /**\n * @param _bufferSize The size of the buffer to replay on subscription\n * @param _windowTime The amount of time the buffered items will stay buffered\n * @param _timestampProvider An object with a `now()` method that provides the current timestamp. This is used to\n * calculate the amount of time something has been buffered.\n */\n constructor(\n private _bufferSize = Infinity,\n private _windowTime = Infinity,\n private _timestampProvider: TimestampProvider = dateTimestampProvider\n ) {\n super();\n this._infiniteTimeWindow = _windowTime === Infinity;\n this._bufferSize = Math.max(1, _bufferSize);\n this._windowTime = Math.max(1, _windowTime);\n }\n\n next(value: T): void {\n const { isStopped, _buffer, _infiniteTimeWindow, _timestampProvider, _windowTime } = this;\n if (!isStopped) {\n _buffer.push(value);\n !_infiniteTimeWindow && _buffer.push(_timestampProvider.now() + _windowTime);\n }\n this._trimBuffer();\n super.next(value);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._trimBuffer();\n\n const subscription = this._innerSubscribe(subscriber);\n\n const { _infiniteTimeWindow, _buffer } = this;\n // We use a copy here, so reentrant code does not mutate our array while we're\n // emitting it to a new subscriber.\n const copy = _buffer.slice();\n for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) {\n subscriber.next(copy[i] as T);\n }\n\n this._checkFinalizedStatuses(subscriber);\n\n return subscription;\n }\n\n private _trimBuffer() {\n const { _bufferSize, _timestampProvider, _buffer, _infiniteTimeWindow } = this;\n // If we don't have an infinite buffer size, and we're over the length,\n // use splice to truncate the old buffer values off. Note that we have to\n // double the size for instances where we're not using an infinite time window\n // because we're storing the values and the timestamps in the same array.\n const adjustedBufferSize = (_infiniteTimeWindow ? 1 : 2) * _bufferSize;\n _bufferSize < Infinity && adjustedBufferSize < _buffer.length && _buffer.splice(0, _buffer.length - adjustedBufferSize);\n\n // Now, if we're not in an infinite time window, remove all values where the time is\n // older than what is allowed.\n if (!_infiniteTimeWindow) {\n const now = _timestampProvider.now();\n let last = 0;\n // Search the array for the first timestamp that isn't expired and\n // truncate the buffer up to that point.\n for (let i = 1; i < _buffer.length && (_buffer[i] as number) <= now; i += 2) {\n last = i;\n }\n last && _buffer.splice(0, last + 1);\n }\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Subscription } from '../Subscription';\nimport { SchedulerAction } from '../types';\n\n/**\n * A unit of work to be executed in a `scheduler`. An action is typically\n * created from within a {@link SchedulerLike} and an RxJS user does not need to concern\n * themselves about creating and manipulating an Action.\n *\n * ```ts\n * class Action extends Subscription {\n * new (scheduler: Scheduler, work: (state?: T) => void);\n * schedule(state?: T, delay: number = 0): Subscription;\n * }\n * ```\n */\nexport class Action extends Subscription {\n constructor(scheduler: Scheduler, work: (this: SchedulerAction, state?: T) => void) {\n super();\n }\n /**\n * Schedules this action on its parent {@link SchedulerLike} for execution. May be passed\n * some context object, `state`. May happen at some point in the future,\n * according to the `delay` parameter, if specified.\n * @param state Some contextual data that the `work` function uses when called by the\n * Scheduler.\n * @param delay Time to wait before executing the work, where the time unit is implicit\n * and defined by the Scheduler.\n * @return A subscription in order to be able to unsubscribe the scheduled work.\n */\n public schedule(state?: T, delay: number = 0): Subscription {\n return this;\n }\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetIntervalFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearIntervalFunction = (handle: TimerHandle) => void;\n\ninterface IntervalProvider {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n delegate:\n | {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n }\n | undefined;\n}\n\nexport const intervalProvider: IntervalProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setInterval(handler: () => void, timeout?: number, ...args) {\n const { delegate } = intervalProvider;\n if (delegate?.setInterval) {\n return delegate.setInterval(handler, timeout, ...args);\n }\n return setInterval(handler, timeout, ...args);\n },\n clearInterval(handle) {\n const { delegate } = intervalProvider;\n return (delegate?.clearInterval || clearInterval)(handle as any);\n },\n delegate: undefined,\n};\n", "import { Action } from './Action';\nimport { SchedulerAction } from '../types';\nimport { Subscription } from '../Subscription';\nimport { AsyncScheduler } from './AsyncScheduler';\nimport { intervalProvider } from './intervalProvider';\nimport { arrRemove } from '../util/arrRemove';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncAction extends Action {\n public id: TimerHandle | undefined;\n public state?: T;\n // @ts-ignore: Property has no initializer and is not definitely assigned\n public delay: number;\n protected pending: boolean = false;\n\n constructor(protected scheduler: AsyncScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (this.closed) {\n return this;\n }\n\n // Always replace the current state with the new state.\n this.state = state;\n\n const id = this.id;\n const scheduler = this.scheduler;\n\n //\n // Important implementation note:\n //\n // Actions only execute once by default, unless rescheduled from within the\n // scheduled callback. This allows us to implement single and repeat\n // actions via the same code path, without adding API surface area, as well\n // as mimic traditional recursion but across asynchronous boundaries.\n //\n // However, JS runtimes and timers distinguish between intervals achieved by\n // serial `setTimeout` calls vs. a single `setInterval` call. An interval of\n // serial `setTimeout` calls can be individually delayed, which delays\n // scheduling the next `setTimeout`, and so on. `setInterval` attempts to\n // guarantee the interval callback will be invoked more precisely to the\n // interval period, regardless of load.\n //\n // Therefore, we use `setInterval` to schedule single and repeat actions.\n // If the action reschedules itself with the same delay, the interval is not\n // canceled. If the action doesn't reschedule, or reschedules with a\n // different delay, the interval will be canceled after scheduled callback\n // execution.\n //\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, delay);\n }\n\n // Set the pending flag indicating that this action has been scheduled, or\n // has recursively rescheduled itself.\n this.pending = true;\n\n this.delay = delay;\n // If this action has already an async Id, don't request a new one.\n this.id = this.id ?? this.requestAsyncId(scheduler, this.id, delay);\n\n return this;\n }\n\n protected requestAsyncId(scheduler: AsyncScheduler, _id?: TimerHandle, delay: number = 0): TimerHandle {\n return intervalProvider.setInterval(scheduler.flush.bind(scheduler, this), delay);\n }\n\n protected recycleAsyncId(_scheduler: AsyncScheduler, id?: TimerHandle, delay: number | null = 0): TimerHandle | undefined {\n // If this action is rescheduled with the same delay time, don't clear the interval id.\n if (delay != null && this.delay === delay && this.pending === false) {\n return id;\n }\n // Otherwise, if the action's delay time is different from the current delay,\n // or the action has been rescheduled before it's executed, clear the interval id\n if (id != null) {\n intervalProvider.clearInterval(id);\n }\n\n return undefined;\n }\n\n /**\n * Immediately executes this action and the `work` it contains.\n */\n public execute(state: T, delay: number): any {\n if (this.closed) {\n return new Error('executing a cancelled action');\n }\n\n this.pending = false;\n const error = this._execute(state, delay);\n if (error) {\n return error;\n } else if (this.pending === false && this.id != null) {\n // Dequeue if the action didn't reschedule itself. Don't call\n // unsubscribe(), because the action could reschedule later.\n // For example:\n // ```\n // scheduler.schedule(function doWork(counter) {\n // /* ... I'm a busy worker bee ... */\n // var originalAction = this;\n // /* wait 100ms before rescheduling the action */\n // setTimeout(function () {\n // originalAction.schedule(counter + 1);\n // }, 100);\n // }, 1000);\n // ```\n this.id = this.recycleAsyncId(this.scheduler, this.id, null);\n }\n }\n\n protected _execute(state: T, _delay: number): any {\n let errored: boolean = false;\n let errorValue: any;\n try {\n this.work(state);\n } catch (e) {\n errored = true;\n // HACK: Since code elsewhere is relying on the \"truthiness\" of the\n // return here, we can't have it return \"\" or 0 or false.\n // TODO: Clean this up when we refactor schedulers mid-version-8 or so.\n errorValue = e ? e : new Error('Scheduled action threw falsy error');\n }\n if (errored) {\n this.unsubscribe();\n return errorValue;\n }\n }\n\n unsubscribe() {\n if (!this.closed) {\n const { id, scheduler } = this;\n const { actions } = scheduler;\n\n this.work = this.state = this.scheduler = null!;\n this.pending = false;\n\n arrRemove(actions, this);\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, null);\n }\n\n this.delay = null!;\n super.unsubscribe();\n }\n }\n}\n", "import { Action } from './scheduler/Action';\nimport { Subscription } from './Subscription';\nimport { SchedulerLike, SchedulerAction } from './types';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * An execution context and a data structure to order tasks and schedule their\n * execution. Provides a notion of (potentially virtual) time, through the\n * `now()` getter method.\n *\n * Each unit of work in a Scheduler is called an `Action`.\n *\n * ```ts\n * class Scheduler {\n * now(): number;\n * schedule(work, delay?, state?): Subscription;\n * }\n * ```\n *\n * @deprecated Scheduler is an internal implementation detail of RxJS, and\n * should not be used directly. Rather, create your own class and implement\n * {@link SchedulerLike}. Will be made internal in v8.\n */\nexport class Scheduler implements SchedulerLike {\n public static now: () => number = dateTimestampProvider.now;\n\n constructor(private schedulerActionCtor: typeof Action, now: () => number = Scheduler.now) {\n this.now = now;\n }\n\n /**\n * A getter method that returns a number representing the current time\n * (at the time this function was called) according to the scheduler's own\n * internal clock.\n * @return A number that represents the current time. May or may not\n * have a relation to wall-clock time. May or may not refer to a time unit\n * (e.g. milliseconds).\n */\n public now: () => number;\n\n /**\n * Schedules a function, `work`, for execution. May happen at some point in\n * the future, according to the `delay` parameter, if specified. May be passed\n * some context object, `state`, which will be passed to the `work` function.\n *\n * The given arguments will be processed an stored as an Action object in a\n * queue of actions.\n *\n * @param work A function representing a task, or some unit of work to be\n * executed by the Scheduler.\n * @param delay Time to wait before executing the work, where the time unit is\n * implicit and defined by the Scheduler itself.\n * @param state Some contextual data that the `work` function uses when called\n * by the Scheduler.\n * @return A subscription in order to be able to unsubscribe the scheduled work.\n */\n public schedule(work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription {\n return new this.schedulerActionCtor(this, work).schedule(state, delay);\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Action } from './Action';\nimport { AsyncAction } from './AsyncAction';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncScheduler extends Scheduler {\n public actions: Array> = [];\n /**\n * A flag to indicate whether the Scheduler is currently executing a batch of\n * queued actions.\n * @internal\n */\n public _active: boolean = false;\n /**\n * An internal ID used to track the latest asynchronous task such as those\n * coming from `setTimeout`, `setInterval`, `requestAnimationFrame`, and\n * others.\n * @internal\n */\n public _scheduled: TimerHandle | undefined;\n\n constructor(SchedulerAction: typeof Action, now: () => number = Scheduler.now) {\n super(SchedulerAction, now);\n }\n\n public flush(action: AsyncAction): void {\n const { actions } = this;\n\n if (this._active) {\n actions.push(action);\n return;\n }\n\n let error: any;\n this._active = true;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions.shift()!)); // exhaust the scheduler queue\n\n this._active = false;\n\n if (error) {\n while ((action = actions.shift()!)) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\n/**\n *\n * Async Scheduler\n *\n * Schedule task as if you used setTimeout(task, duration)\n *\n * `async` scheduler schedules tasks asynchronously, by putting them on the JavaScript\n * event loop queue. It is best used to delay tasks in time or to schedule tasks repeating\n * in intervals.\n *\n * If you just want to \"defer\" task, that is to perform it right after currently\n * executing synchronous code ends (commonly achieved by `setTimeout(deferredTask, 0)`),\n * better choice will be the {@link asapScheduler} scheduler.\n *\n * ## Examples\n * Use async scheduler to delay task\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * const task = () => console.log('it works!');\n *\n * asyncScheduler.schedule(task, 2000);\n *\n * // After 2 seconds logs:\n * // \"it works!\"\n * ```\n *\n * Use async scheduler to repeat task in intervals\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * function task(state) {\n * console.log(state);\n * this.schedule(state + 1, 1000); // `this` references currently executing Action,\n * // which we reschedule with new state and delay\n * }\n *\n * asyncScheduler.schedule(task, 3000, 0);\n *\n * // Logs:\n * // 0 after 3s\n * // 1 after 4s\n * // 2 after 5s\n * // 3 after 6s\n * ```\n */\n\nexport const asyncScheduler = new AsyncScheduler(AsyncAction);\n\n/**\n * @deprecated Renamed to {@link asyncScheduler}. Will be removed in v8.\n */\nexport const async = asyncScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { Subscription } from '../Subscription';\nimport { QueueScheduler } from './QueueScheduler';\nimport { SchedulerAction } from '../types';\nimport { TimerHandle } from './timerHandle';\n\nexport class QueueAction extends AsyncAction {\n constructor(protected scheduler: QueueScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (delay > 0) {\n return super.schedule(state, delay);\n }\n this.delay = delay;\n this.state = state;\n this.scheduler.flush(this);\n return this;\n }\n\n public execute(state: T, delay: number): any {\n return delay > 0 || this.closed ? super.execute(state, delay) : this._execute(state, delay);\n }\n\n protected requestAsyncId(scheduler: QueueScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n\n if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n\n // Otherwise flush the scheduler starting with this action.\n scheduler.flush(this);\n\n // HACK: In the past, this was returning `void`. However, `void` isn't a valid\n // `TimerHandle`, and generally the return value here isn't really used. So the\n // compromise is to return `0` which is both \"falsy\" and a valid `TimerHandle`,\n // as opposed to refactoring every other instanceo of `requestAsyncId`.\n return 0;\n }\n}\n", "import { AsyncScheduler } from './AsyncScheduler';\n\nexport class QueueScheduler extends AsyncScheduler {\n}\n", "import { QueueAction } from './QueueAction';\nimport { QueueScheduler } from './QueueScheduler';\n\n/**\n *\n * Queue Scheduler\n *\n * Put every next task on a queue, instead of executing it immediately\n *\n * `queue` scheduler, when used with delay, behaves the same as {@link asyncScheduler} scheduler.\n *\n * When used without delay, it schedules given task synchronously - executes it right when\n * it is scheduled. However when called recursively, that is when inside the scheduled task,\n * another task is scheduled with queue scheduler, instead of executing immediately as well,\n * that task will be put on a queue and wait for current one to finish.\n *\n * This means that when you execute task with `queue` scheduler, you are sure it will end\n * before any other task scheduled with that scheduler will start.\n *\n * ## Examples\n * Schedule recursively first, then do something\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(() => {\n * queueScheduler.schedule(() => console.log('second')); // will not happen now, but will be put on a queue\n *\n * console.log('first');\n * });\n *\n * // Logs:\n * // \"first\"\n * // \"second\"\n * ```\n *\n * Reschedule itself recursively\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(function(state) {\n * if (state !== 0) {\n * console.log('before', state);\n * this.schedule(state - 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * console.log('after', state);\n * }\n * }, 0, 3);\n *\n * // In scheduler that runs recursively, you would expect:\n * // \"before\", 3\n * // \"before\", 2\n * // \"before\", 1\n * // \"after\", 1\n * // \"after\", 2\n * // \"after\", 3\n *\n * // But with queue it logs:\n * // \"before\", 3\n * // \"after\", 3\n * // \"before\", 2\n * // \"after\", 2\n * // \"before\", 1\n * // \"after\", 1\n * ```\n */\n\nexport const queueScheduler = new QueueScheduler(QueueAction);\n\n/**\n * @deprecated Renamed to {@link queueScheduler}. Will be removed in v8.\n */\nexport const queue = queueScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\nimport { SchedulerAction } from '../types';\nimport { animationFrameProvider } from './animationFrameProvider';\nimport { TimerHandle } from './timerHandle';\n\nexport class AnimationFrameAction extends AsyncAction {\n constructor(protected scheduler: AnimationFrameScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay is greater than 0, request as an async action.\n if (delay !== null && delay > 0) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n // Push the action to the end of the scheduler queue.\n scheduler.actions.push(this);\n // If an animation frame has already been requested, don't request another\n // one. If an animation frame hasn't been requested yet, request one. Return\n // the current animation frame request id.\n return scheduler._scheduled || (scheduler._scheduled = animationFrameProvider.requestAnimationFrame(() => scheduler.flush(undefined)));\n }\n\n protected recycleAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n if (delay != null ? delay > 0 : this.delay > 0) {\n return super.recycleAsyncId(scheduler, id, delay);\n }\n // If the scheduler queue has no remaining actions with the same async id,\n // cancel the requested animation frame and set the scheduled flag to\n // undefined so the next AnimationFrameAction will request its own.\n const { actions } = scheduler;\n if (id != null && id === scheduler._scheduled && actions[actions.length - 1]?.id !== id) {\n animationFrameProvider.cancelAnimationFrame(id as number);\n scheduler._scheduled = undefined;\n }\n // Return undefined so the action knows to request a new async id if it's rescheduled.\n return undefined;\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\nexport class AnimationFrameScheduler extends AsyncScheduler {\n public flush(action?: AsyncAction): void {\n this._active = true;\n // The async id that effects a call to flush is stored in _scheduled.\n // Before executing an action, it's necessary to check the action's async\n // id to determine whether it's supposed to be executed in the current\n // flush.\n // Previous implementations of this method used a count to determine this,\n // but that was unsound, as actions that are unsubscribed - i.e. cancelled -\n // are removed from the actions array and that can shift actions that are\n // scheduled to be executed in a subsequent flush into positions at which\n // they are executed within the current flush.\n let flushId;\n if (action) {\n flushId = action.id;\n } else {\n flushId = this._scheduled;\n this._scheduled = undefined;\n }\n\n const { actions } = this;\n let error: any;\n action = action || actions.shift()!;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions[0]) && action.id === flushId && actions.shift());\n\n this._active = false;\n\n if (error) {\n while ((action = actions[0]) && action.id === flushId && actions.shift()) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AnimationFrameAction } from './AnimationFrameAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\n\n/**\n *\n * Animation Frame Scheduler\n *\n * Perform task when `window.requestAnimationFrame` would fire\n *\n * When `animationFrame` scheduler is used with delay, it will fall back to {@link asyncScheduler} scheduler\n * behaviour.\n *\n * Without delay, `animationFrame` scheduler can be used to create smooth browser animations.\n * It makes sure scheduled task will happen just before next browser content repaint,\n * thus performing animations as efficiently as possible.\n *\n * ## Example\n * Schedule div height animation\n * ```ts\n * // html:
\n * import { animationFrameScheduler } from 'rxjs';\n *\n * const div = document.querySelector('div');\n *\n * animationFrameScheduler.schedule(function(height) {\n * div.style.height = height + \"px\";\n *\n * this.schedule(height + 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * }, 0, 0);\n *\n * // You will see a div element growing in height\n * ```\n */\n\nexport const animationFrameScheduler = new AnimationFrameScheduler(AnimationFrameAction);\n\n/**\n * @deprecated Renamed to {@link animationFrameScheduler}. Will be removed in v8.\n */\nexport const animationFrame = animationFrameScheduler;\n", "import { Observable } from '../Observable';\nimport { SchedulerLike } from '../types';\n\n/**\n * A simple Observable that emits no items to the Observer and immediately\n * emits a complete notification.\n *\n * Just emits 'complete', and nothing else.\n *\n * ![](empty.png)\n *\n * A simple Observable that only emits the complete notification. It can be used\n * for composing with other Observables, such as in a {@link mergeMap}.\n *\n * ## Examples\n *\n * Log complete notification\n *\n * ```ts\n * import { EMPTY } from 'rxjs';\n *\n * EMPTY.subscribe({\n * next: () => console.log('Next'),\n * complete: () => console.log('Complete!')\n * });\n *\n * // Outputs\n * // Complete!\n * ```\n *\n * Emit the number 7, then complete\n *\n * ```ts\n * import { EMPTY, startWith } from 'rxjs';\n *\n * const result = EMPTY.pipe(startWith(7));\n * result.subscribe(x => console.log(x));\n *\n * // Outputs\n * // 7\n * ```\n *\n * Map and flatten only odd numbers to the sequence `'a'`, `'b'`, `'c'`\n *\n * ```ts\n * import { interval, mergeMap, of, EMPTY } from 'rxjs';\n *\n * const interval$ = interval(1000);\n * const result = interval$.pipe(\n * mergeMap(x => x % 2 === 1 ? of('a', 'b', 'c') : EMPTY),\n * );\n * result.subscribe(x => console.log(x));\n *\n * // Results in the following to the console:\n * // x is equal to the count on the interval, e.g. (0, 1, 2, 3, ...)\n * // x will occur every 1000ms\n * // if x % 2 is equal to 1, print a, b, c (each on its own)\n * // if x % 2 is not equal to 1, nothing will be output\n * ```\n *\n * @see {@link Observable}\n * @see {@link NEVER}\n * @see {@link of}\n * @see {@link throwError}\n */\nexport const EMPTY = new Observable((subscriber) => subscriber.complete());\n\n/**\n * @param scheduler A {@link SchedulerLike} to use for scheduling\n * the emission of the complete notification.\n * @deprecated Replaced with the {@link EMPTY} constant or {@link scheduled} (e.g. `scheduled([], scheduler)`). Will be removed in v8.\n */\nexport function empty(scheduler?: SchedulerLike) {\n return scheduler ? emptyScheduled(scheduler) : EMPTY;\n}\n\nfunction emptyScheduled(scheduler: SchedulerLike) {\n return new Observable((subscriber) => scheduler.schedule(() => subscriber.complete()));\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport function isScheduler(value: any): value is SchedulerLike {\n return value && isFunction(value.schedule);\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\nimport { isScheduler } from './isScheduler';\n\nfunction last(arr: T[]): T | undefined {\n return arr[arr.length - 1];\n}\n\nexport function popResultSelector(args: any[]): ((...args: unknown[]) => unknown) | undefined {\n return isFunction(last(args)) ? args.pop() : undefined;\n}\n\nexport function popScheduler(args: any[]): SchedulerLike | undefined {\n return isScheduler(last(args)) ? args.pop() : undefined;\n}\n\nexport function popNumber(args: any[], defaultValue: number): number {\n return typeof last(args) === 'number' ? args.pop()! : defaultValue;\n}\n", "export const isArrayLike = ((x: any): x is ArrayLike => x && typeof x.length === 'number' && typeof x !== 'function');", "import { isFunction } from \"./isFunction\";\n\n/**\n * Tests to see if the object is \"thennable\".\n * @param value the object to test\n */\nexport function isPromise(value: any): value is PromiseLike {\n return isFunction(value?.then);\n}\n", "import { InteropObservable } from '../types';\nimport { observable as Symbol_observable } from '../symbol/observable';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being Observable (but not necessary an Rx Observable) */\nexport function isInteropObservable(input: any): input is InteropObservable {\n return isFunction(input[Symbol_observable]);\n}\n", "import { isFunction } from './isFunction';\n\nexport function isAsyncIterable(obj: any): obj is AsyncIterable {\n return Symbol.asyncIterator && isFunction(obj?.[Symbol.asyncIterator]);\n}\n", "/**\n * Creates the TypeError to throw if an invalid object is passed to `from` or `scheduled`.\n * @param input The object that was passed.\n */\nexport function createInvalidObservableTypeError(input: any) {\n // TODO: We should create error codes that can be looked up, so this can be less verbose.\n return new TypeError(\n `You provided ${\n input !== null && typeof input === 'object' ? 'an invalid object' : `'${input}'`\n } where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`\n );\n}\n", "export function getSymbolIterator(): symbol {\n if (typeof Symbol !== 'function' || !Symbol.iterator) {\n return '@@iterator' as any;\n }\n\n return Symbol.iterator;\n}\n\nexport const iterator = getSymbolIterator();\n", "import { iterator as Symbol_iterator } from '../symbol/iterator';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being an Iterable */\nexport function isIterable(input: any): input is Iterable {\n return isFunction(input?.[Symbol_iterator]);\n}\n", "import { ReadableStreamLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport async function* readableStreamLikeToAsyncGenerator(readableStream: ReadableStreamLike): AsyncGenerator {\n const reader = readableStream.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n return;\n }\n yield value!;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\nexport function isReadableStreamLike(obj: any): obj is ReadableStreamLike {\n // We don't want to use instanceof checks because they would return\n // false for instances from another Realm, like an