chore: simplify project to engine docs and tests
Remove planning and acceptance scaffolding while retaining the native Vulkan mission preview, format readers, runtime algorithms, and ordinary Rust tests. Keep the book aligned with the runnable project and validate checked-in shaders without generated tool metadata.
This commit is contained in:
@@ -1,2 +0,0 @@
|
|||||||
[alias]
|
|
||||||
xtask = "run -p xtask --"
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"image": "mcr.microsoft.com/devcontainers/rust:latest",
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"extensions": ["rust-lang.rust-analyzer"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runArgs": ["--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined"]
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
name: RenovateBot
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "@daily"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
renovate:
|
|
||||||
container: ghcr.io/renovatebot/renovate:44
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
|
|
||||||
- name: Run renovate
|
|
||||||
run: |
|
|
||||||
renovate
|
|
||||||
env:
|
|
||||||
GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_TOKEN }}
|
|
||||||
LOG_LEVEL: ${{ vars.RENOVATE_LOG_LEVEL }}
|
|
||||||
RENOVATE_CONFIG_FILE: renovate.config.cjs
|
|
||||||
RENOVATE_LOG_LEVEL: ${{ vars.RENOVATE_LOG_LEVEL }}
|
|
||||||
RENOVATE_REPOSITORIES: ${{ gitea.repository }}
|
|
||||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
name: Test
|
|
||||||
|
|
||||||
on: [push, pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
name: Lint
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
components: clippy
|
|
||||||
- name: Cargo check
|
|
||||||
run: cargo check --workspace --all-targets --all-features
|
|
||||||
- name: Clippy (deny warnings)
|
|
||||||
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
|
|
||||||
|
|
||||||
test:
|
|
||||||
name: Test
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: lint
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
- name: Cargo test
|
|
||||||
run: cargo test --workspace --all-features -- --nocapture
|
|
||||||
|
|
||||||
render-parity:
|
|
||||||
name: Render parity
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: test
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
- name: Install headless GL runtime
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y xvfb libgl1-mesa-dri libgles2-mesa-dev mesa-utils
|
|
||||||
- name: Build render-demo binary
|
|
||||||
run: cargo build -p render-demo --features demo
|
|
||||||
- name: Run frame parity suite
|
|
||||||
run: |
|
|
||||||
xvfb-run -s "-screen 0 1280x720x24" cargo run -p render-parity -- \
|
|
||||||
--manifest parity/cases.toml \
|
|
||||||
--output-dir target/render-parity/current \
|
|
||||||
--demo-bin target/debug/parkan-render-demo \
|
|
||||||
--keep-going
|
|
||||||
- name: Upload parity artifacts
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: render-parity-artifacts
|
|
||||||
path: target/render-parity/current
|
|
||||||
if-no-files-found: ignore
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
name: fparkan-ci
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [devel, main]
|
|
||||||
pull_request:
|
|
||||||
branches: [devel, main]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
msrv-backend-neutral:
|
|
||||||
name: Pinned Rust backend-neutral crates
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 20
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
|
||||||
- uses: dtolnay/rust-toolchain@67ef31d5b988238dd797d409d6f9574278e20537
|
|
||||||
with:
|
|
||||||
toolchain: 1.98.1
|
|
||||||
- name: Test backend-neutral crates
|
|
||||||
run: >
|
|
||||||
cargo test
|
|
||||||
-p fparkan-animation
|
|
||||||
-p fparkan-binary
|
|
||||||
-p fparkan-corpus
|
|
||||||
-p fparkan-diagnostics
|
|
||||||
-p fparkan-fx
|
|
||||||
-p fparkan-inspection
|
|
||||||
-p fparkan-material
|
|
||||||
-p fparkan-mission-format
|
|
||||||
-p fparkan-msh
|
|
||||||
-p fparkan-nres
|
|
||||||
-p fparkan-path
|
|
||||||
-p fparkan-platform
|
|
||||||
-p fparkan-prototype
|
|
||||||
-p fparkan-render
|
|
||||||
-p fparkan-resource
|
|
||||||
-p fparkan-rsli
|
|
||||||
-p fparkan-runtime
|
|
||||||
-p fparkan-terrain
|
|
||||||
-p fparkan-terrain-format
|
|
||||||
-p fparkan-texm
|
|
||||||
-p fparkan-vfs
|
|
||||||
-p fparkan-world
|
|
||||||
--all-targets
|
|
||||||
--locked
|
|
||||||
|
|
||||||
stage0-matrix:
|
|
||||||
name: Stage 0 CI (${{ matrix.os }})
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
timeout-minutes: 30
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: macos-15
|
|
||||||
smoke_platform: macos
|
|
||||||
runner_arch: arm64
|
|
||||||
moltenvk_version: 1.4.1
|
|
||||||
vulkan_sdk_version: 1.4.350.1
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
|
||||||
- uses: dtolnay/rust-toolchain@67ef31d5b988238dd797d409d6f9574278e20537
|
|
||||||
with:
|
|
||||||
toolchain: 1.98.1
|
|
||||||
components: clippy,rustfmt
|
|
||||||
- name: Provision macOS Vulkan runtime
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
brew install molten-vk vulkan-loader vulkan-tools vulkan-validationlayers
|
|
||||||
test "$(uname -m)" = "${{ matrix.runner_arch }}"
|
|
||||||
ruby <<'RUBY'
|
|
||||||
require "json"
|
|
||||||
|
|
||||||
expected = {
|
|
||||||
"molten-vk" => "${{ matrix.moltenvk_version }}",
|
|
||||||
"vulkan-loader" => "${{ matrix.vulkan_sdk_version }}",
|
|
||||||
"vulkan-tools" => "${{ matrix.vulkan_sdk_version }}",
|
|
||||||
"vulkan-validationlayers" => "${{ matrix.vulkan_sdk_version }}",
|
|
||||||
}
|
|
||||||
payload = JSON.parse(`brew info --json=v2 #{expected.keys.join(" ")}`)
|
|
||||||
actual = payload.fetch("formulae").to_h do |formula|
|
|
||||||
[formula.fetch("name"), formula.fetch("versions").fetch("stable")]
|
|
||||||
end
|
|
||||||
mismatches = expected.each_with_object({}) do |(name, version), out|
|
|
||||||
actual_version = actual[name]
|
|
||||||
next if actual_version == version
|
|
||||||
|
|
||||||
out[name] = {
|
|
||||||
"expected" => version,
|
|
||||||
"actual" => actual_version,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
unless mismatches.empty?
|
|
||||||
warn JSON.pretty_generate(mismatches)
|
|
||||||
abort "unexpected macOS Vulkan formula version"
|
|
||||||
end
|
|
||||||
RUBY
|
|
||||||
HOMEBREW_PREFIX="$(brew --prefix)"
|
|
||||||
VK_ICD_FILENAMES="$HOMEBREW_PREFIX/opt/molten-vk/etc/vulkan/icd.d/MoltenVK_icd.json"
|
|
||||||
VK_LAYER_PATH="$HOMEBREW_PREFIX/opt/vulkan-validationlayers/share/vulkan/explicit_layer.d"
|
|
||||||
DYLD_FALLBACK_LIBRARY_PATH="$HOMEBREW_PREFIX/opt/vulkan-loader/lib:$HOMEBREW_PREFIX/opt/molten-vk/lib"
|
|
||||||
test -f "$VK_ICD_FILENAMES"
|
|
||||||
test -d "$VK_LAYER_PATH"
|
|
||||||
echo "VK_ICD_FILENAMES=$VK_ICD_FILENAMES" >> "$GITHUB_ENV"
|
|
||||||
echo "VK_LAYER_PATH=$VK_LAYER_PATH" >> "$GITHUB_ENV"
|
|
||||||
echo "DYLD_FALLBACK_LIBRARY_PATH=$DYLD_FALLBACK_LIBRARY_PATH" >> "$GITHUB_ENV"
|
|
||||||
- name: Install cargo-deny
|
|
||||||
run: cargo install cargo-deny --version 0.19.9 --locked
|
|
||||||
- name: Verify shader provenance
|
|
||||||
run: cargo xtask shader-provenance
|
|
||||||
- name: Run canonical CI gate
|
|
||||||
run: cargo xtask ci
|
|
||||||
- name: Run native Vulkan smoke
|
|
||||||
run: >
|
|
||||||
cargo run -p fparkan-vulkan-smoke --locked --
|
|
||||||
--out "target/fparkan/native-smoke/${{ matrix.smoke_platform }}.json"
|
|
||||||
--timeout-seconds 120
|
|
||||||
- name: Upload acceptance audit
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
|
||||||
with:
|
|
||||||
name: stage-0-acceptance-${{ matrix.os }}
|
|
||||||
path: target/fparkan/acceptance/stage-0-audit.json
|
|
||||||
if-no-files-found: error
|
|
||||||
- name: Upload native smoke report
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
|
||||||
with:
|
|
||||||
name: native-smoke-${{ matrix.smoke_platform }}
|
|
||||||
path: target/fparkan/native-smoke/*.json
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
native-smoke-audit:
|
|
||||||
name: Native smoke audit
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
needs: stage0-matrix
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
|
||||||
- uses: dtolnay/rust-toolchain@67ef31d5b988238dd797d409d6f9574278e20537
|
|
||||||
with:
|
|
||||||
toolchain: 1.98.1
|
|
||||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
|
||||||
with:
|
|
||||||
pattern: native-smoke-*
|
|
||||||
path: target/fparkan/native-smoke-artifacts
|
|
||||||
merge-multiple: true
|
|
||||||
- name: Aggregate native smoke reports
|
|
||||||
run: >
|
|
||||||
cargo xtask native-smoke audit
|
|
||||||
--dir target/fparkan/native-smoke-artifacts
|
|
||||||
+13
-209
@@ -1,214 +1,18 @@
|
|||||||
*~
|
# Rust build output
|
||||||
|
/target/
|
||||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
|
||||||
.fuse_hidden*
|
|
||||||
|
|
||||||
# KDE directory preferences
|
|
||||||
.directory
|
|
||||||
|
|
||||||
# Linux trash folder which might appear on any partition or disk
|
|
||||||
.Trash-*
|
|
||||||
|
|
||||||
# .nfs files are created when an open file is removed but is still being accessed
|
|
||||||
.nfs*
|
|
||||||
|
|
||||||
# General
|
|
||||||
.DS_Store
|
|
||||||
.AppleDouble
|
|
||||||
.LSOverride
|
|
||||||
|
|
||||||
# Icon must end with two \r
|
|
||||||
Icon
|
|
||||||
|
|
||||||
# Thumbnails
|
|
||||||
._*
|
|
||||||
|
|
||||||
# Files that might appear in the root of a volume
|
|
||||||
.DocumentRevisions-V100
|
|
||||||
.fseventsd
|
|
||||||
.Spotlight-V100
|
|
||||||
.TemporaryItems
|
|
||||||
.Trashes
|
|
||||||
.VolumeIcon.icns
|
|
||||||
.com.apple.timemachine.donotpresent
|
|
||||||
|
|
||||||
# Directories potentially created on remote AFP share
|
|
||||||
.AppleDB
|
|
||||||
.AppleDesktop
|
|
||||||
Network Trash Folder
|
|
||||||
Temporary Items
|
|
||||||
.apdisk
|
|
||||||
|
|
||||||
# Windows thumbnail cache files
|
|
||||||
Thumbs.db
|
|
||||||
Thumbs.db:encryptable
|
|
||||||
ehthumbs.db
|
|
||||||
ehthumbs_vista.db
|
|
||||||
|
|
||||||
# Dump file
|
|
||||||
*.stackdump
|
|
||||||
|
|
||||||
# Folder config file
|
|
||||||
[Dd]esktop.ini
|
|
||||||
|
|
||||||
# Recycle Bin used on file shares
|
|
||||||
$RECYCLE.BIN/
|
|
||||||
|
|
||||||
# Windows Installer files
|
|
||||||
*.cab
|
|
||||||
*.msi
|
|
||||||
*.msix
|
|
||||||
*.msm
|
|
||||||
*.msp
|
|
||||||
|
|
||||||
# Windows shortcuts
|
|
||||||
*.lnk
|
|
||||||
|
|
||||||
# Generated by Cargo
|
|
||||||
# will have compiled files and executables
|
|
||||||
debug/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|
||||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
|
||||||
*.pdb
|
*.pdb
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Local licensed game data and research output
|
||||||
__pycache__/
|
/testdata/*
|
||||||
*.py[cod]
|
!/testdata/.gitignore
|
||||||
*$py.class
|
/target-research/
|
||||||
|
*.capture
|
||||||
|
*.dump
|
||||||
|
|
||||||
# C extensions
|
# Local configuration and secrets
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
tmp/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
.pdm.toml
|
|
||||||
|
|
||||||
# PEP 582
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
.env
|
||||||
.venv
|
.env.*
|
||||||
env/
|
*.pem
|
||||||
venv/
|
*.key
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# Poetry local configuration file
|
|
||||||
poetry.toml
|
|
||||||
|
|
||||||
# ruff
|
|
||||||
.ruff_cache/
|
|
||||||
|
|
||||||
# LSP config files
|
|
||||||
pyrightconfig.json
|
|
||||||
|
|||||||
-30
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"extends": [
|
|
||||||
"config:recommended",
|
|
||||||
":disableDependencyDashboard"
|
|
||||||
],
|
|
||||||
"assignees": [
|
|
||||||
"valentineus"
|
|
||||||
],
|
|
||||||
"labels": [
|
|
||||||
"dependencies",
|
|
||||||
"automated"
|
|
||||||
],
|
|
||||||
"packageRules": [
|
|
||||||
{
|
|
||||||
"groupName": "all digest updates",
|
|
||||||
"groupSlug": "all-digest",
|
|
||||||
"matchUpdateTypes": [
|
|
||||||
"minor",
|
|
||||||
"patch",
|
|
||||||
"pin",
|
|
||||||
"digest"
|
|
||||||
],
|
|
||||||
"matchPackageNames": [
|
|
||||||
"*"
|
|
||||||
],
|
|
||||||
"automerge": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Generated
-1884
File diff suppressed because it is too large
Load Diff
-29
@@ -4,10 +4,7 @@ members = [
|
|||||||
"crates/fparkan-animation",
|
"crates/fparkan-animation",
|
||||||
"crates/fparkan-assets",
|
"crates/fparkan-assets",
|
||||||
"crates/fparkan-binary",
|
"crates/fparkan-binary",
|
||||||
"crates/fparkan-corpus",
|
|
||||||
"crates/fparkan-diagnostics",
|
|
||||||
"crates/fparkan-fx",
|
"crates/fparkan-fx",
|
||||||
"crates/fparkan-inspection",
|
|
||||||
"crates/fparkan-material",
|
"crates/fparkan-material",
|
||||||
"crates/fparkan-mission-format",
|
"crates/fparkan-mission-format",
|
||||||
"crates/fparkan-msh",
|
"crates/fparkan-msh",
|
||||||
@@ -22,46 +19,20 @@ members = [
|
|||||||
"crates/fparkan-runtime",
|
"crates/fparkan-runtime",
|
||||||
"crates/fparkan-terrain",
|
"crates/fparkan-terrain",
|
||||||
"crates/fparkan-terrain-format",
|
"crates/fparkan-terrain-format",
|
||||||
"crates/fparkan-test-support",
|
|
||||||
"crates/fparkan-texm",
|
"crates/fparkan-texm",
|
||||||
"crates/fparkan-vfs",
|
"crates/fparkan-vfs",
|
||||||
"crates/fparkan-world",
|
"crates/fparkan-world",
|
||||||
"adapters/fparkan-platform-winit",
|
"adapters/fparkan-platform-winit",
|
||||||
"adapters/fparkan-render-vulkan",
|
"adapters/fparkan-render-vulkan",
|
||||||
"apps/fparkan-cli",
|
|
||||||
"apps/fparkan-game",
|
"apps/fparkan-game",
|
||||||
"apps/fparkan-headless",
|
|
||||||
"apps/fparkan-vulkan-smoke",
|
|
||||||
"apps/fparkan-viewer",
|
|
||||||
"xtask",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.97"
|
|
||||||
license = "GPL-2.0-only"
|
license = "GPL-2.0-only"
|
||||||
repository = "https://github.com/valentineus/fparkan"
|
repository = "https://github.com/valentineus/fparkan"
|
||||||
|
|
||||||
[workspace.lints.rust]
|
|
||||||
unsafe_code = "forbid"
|
|
||||||
missing_docs = "warn"
|
|
||||||
unreachable_pub = "warn"
|
|
||||||
unused_must_use = "deny"
|
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
|
||||||
all = { level = "deny", priority = -1 }
|
|
||||||
pedantic = { level = "warn", priority = -1 }
|
|
||||||
unwrap_used = "deny"
|
|
||||||
expect_used = "deny"
|
|
||||||
panic = "deny"
|
|
||||||
todo = "deny"
|
|
||||||
unimplemented = "deny"
|
|
||||||
dbg_macro = "deny"
|
|
||||||
print_stdout = "warn"
|
|
||||||
print_stderr = "warn"
|
|
||||||
lossy_float_literal = "deny"
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
lto = true
|
lto = true
|
||||||
|
|||||||
@@ -1,118 +1,60 @@
|
|||||||
# FParkan
|
# FParkan
|
||||||
|
|
||||||
Open source проект с реализацией компонентов игрового движка игры **«Паркан: Железная Стратегия»**.
|
Открытый движок *Паркан: Железная стратегия* на Rust и Vulkan. Он читает
|
||||||
|
ресурсы установленной оригинальной игры. Сейчас приложение загружает миссию
|
||||||
|
и показывает её ландшафт и модели в статическом окне Vulkan.
|
||||||
|
|
||||||
## Описание
|
## Запуск
|
||||||
|
|
||||||
Проект находится в активной разработке и включает:
|
Нужны Rust и драйвер видеокарты с поддержкой Vulkan. Игровые файлы в
|
||||||
|
репозиторий не входят.
|
||||||
|
|
||||||
- библиотеки для работы с форматами игровых архивов;
|
```powershell
|
||||||
- спецификации форматов и сопутствующую документацию.
|
cargo run -p fparkan-game -- `
|
||||||
|
--root 'C:\GOG Games\Parkan - Iron Strategy' `
|
||||||
## Установка
|
--mission 'MISSIONS\Autodemo.00\data.tma' --frames 240
|
||||||
|
|
||||||
Проект находится в начальной стадии, подробная инструкция по установке пока отсутствует.
|
|
||||||
|
|
||||||
## Документация
|
|
||||||
|
|
||||||
- локально: каталог [`docs/`](docs)
|
|
||||||
- сайт: <https://fparkan.popov.link>
|
|
||||||
|
|
||||||
## Библиотеки
|
|
||||||
|
|
||||||
- [crates/fparkan-nres](crates/fparkan-nres) — strict/lossless модель архивов NRes.
|
|
||||||
- [crates/fparkan-rsli](crates/fparkan-rsli) — чтение, lookup и lossless roundtrip архивов RsLi.
|
|
||||||
- [crates/fparkan-msh](crates/fparkan-msh) — validated static MSH geometry.
|
|
||||||
- [crates/fparkan-runtime](crates/fparkan-runtime) — transactional mission loading и headless runtime foundation.
|
|
||||||
- [apps/fparkan-cli](apps/fparkan-cli) — CLI для архивов, графов и acceptance-отчетов.
|
|
||||||
- [apps/fparkan-viewer](apps/fparkan-viewer) — inspection-only CLI для archive/model/texture/map без live Vulkan draw path.
|
|
||||||
- [apps/fparkan-headless](apps/fparkan-headless) — headless runtime composition root.
|
|
||||||
- [apps/fparkan-game](apps/fparkan-game) — mission composition root: по умолчанию выдаёт planning report; opt-in `--backend static-vulkan` открывает native Vulkan окно и рисует terrain и все подготовленные MSH-компоненты выбранных mission roots. По умолчанию выбираются все roots; `--preview-roots N` оставляет bounded diagnostic scope.
|
|
||||||
|
|
||||||
## Текущий статус рендера
|
|
||||||
|
|
||||||
- `fparkan-vulkan-smoke` доказывает живой Stage 0 Vulkan triangle path с native window, swapchain и validation telemetry.
|
|
||||||
- `VulkanPlanningBackend` и default-режим `fparkan-game` подтверждают только deterministic command planning/capture, а не draw пикселей. `fparkan-game --backend static-vulkan` — узкий mission-to-native-Vulkan bridge: он объединяет terrain и подготовленные MSH-компоненты всех выбранных roots, применяет static TMA transforms и загружает первую MAT0 diffuse texture на каждый используемый selector с preview-local remap. Legacy D3D7 camera capture поддерживается; полный corpus, material phases, dynamic ownership/visibility и pixel parity ещё не подтверждены.
|
|
||||||
- `fparkan-viewer` пока является инспектором ассетов. `fparkan-vulkan-smoke` имеет live Stage 3 bridge для original `MSH`/`Texm`/`WEAR`/`MAT0` и geometry-only `Land.msh`; полноценный viewer, исходные terrain-material states, camera и pixel parity ещё не закрыты.
|
|
||||||
- Truth table и evidence-артефакты вынесены в [`docs/rendering/renderer_truth_table.md`](docs/rendering/renderer_truth_table.md) и [`docs/evidence/`](docs/evidence).
|
|
||||||
|
|
||||||
## Тестирование
|
|
||||||
|
|
||||||
Базовое тестирование проходит на синтетических тестах из репозитория:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo xtask ci
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Для дополнительного тестирования на реальных игровых ресурсах:
|
`--frames` задаёт число кадров. `--preview-roots N` ограничивает число объектов,
|
||||||
|
`--legacy-camera-capture` читает сохранённую матрицу камеры,
|
||||||
|
`--static-animation-frame` и `--static-material-phase` выбирают позу и фазу
|
||||||
|
материала. `--readback-out target/frame.raw` сохраняет последний кадр GPU.
|
||||||
|
Без сохранённой камеры используется вид карты сверху.
|
||||||
|
|
||||||
- используйте оригинальную копию игры (диск или [GOG-версия](https://www.gog.com/en/game/parkan_iron_strategy));
|
## Разработка
|
||||||
- разместите игровые каталоги в [`testdata/`](testdata);
|
|
||||||
- игровые ресурсы в репозиторий не включаются, так как защищены авторским правом.
|
|
||||||
|
|
||||||
Локальный licensed gate использует некоммитимый manifest:
|
```text
|
||||||
|
cargo fmt --all -- --check
|
||||||
```bash
|
cargo test --workspace
|
||||||
cat > /private/tmp/fparkan-corpora.toml <<'EOF'
|
cargo check --workspace
|
||||||
schema = 1
|
|
||||||
|
|
||||||
[[corpus]]
|
|
||||||
id = "part1-local"
|
|
||||||
kind = "part1"
|
|
||||||
root = "/absolute/path/to/IS"
|
|
||||||
expected_profile = "parkan-is-part1"
|
|
||||||
|
|
||||||
[[corpus]]
|
|
||||||
id = "part2-local"
|
|
||||||
kind = "part2"
|
|
||||||
root = "/absolute/path/to/IS2"
|
|
||||||
expected_profile = "parkan-is-part2"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
FPARKAN_CORPORA_MANIFEST=/private/tmp/fparkan-corpora.toml \
|
|
||||||
cargo xtask acceptance report --suite licensed --stage 5
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Stage 0 Vulkan smoke
|
Алгоритмы и форматы проверяются обычными Rust-тестами рядом с кодом.
|
||||||
|
Тесты с оригинальными ресурсами помечены `#[ignore]`; пути к двум частям
|
||||||
|
установки задаются через `FPARKAN_CORPUS_PART1_ROOT` и
|
||||||
|
`FPARKAN_CORPUS_PART2_ROOT`. Зависимости разрешает Cargo, локальный
|
||||||
|
`Cargo.lock` не хранится в Git.
|
||||||
|
|
||||||
Локальный Stage 0 smoke запускает реальный `winit` lifecycle и Vulkan triangle path с включёнными validation layers. Успешный прогон обязан:
|
Готовые SPIR-V шейдеры находятся рядом с исходниками GLSL.
|
||||||
|
Vulkan SDK нужен только для их пересборки и проверки с `--validation`.
|
||||||
|
При доступных в PATH инструментах SDK:
|
||||||
|
|
||||||
- отрисовать 300 кадров;
|
```powershell
|
||||||
- выполнить как минимум один реальный resize;
|
glslangValidator -V adapters/fparkan-render-vulkan/shaders/triangle.vert -o adapters/fparkan-render-vulkan/shaders/triangle.vert.spv
|
||||||
- пересоздать swapchain после resize;
|
glslangValidator -V adapters/fparkan-render-vulkan/shaders/triangle.frag -o adapters/fparkan-render-vulkan/shaders/triangle.frag.spv
|
||||||
- завершиться без validation warnings/errors.
|
spirv-val adapters/fparkan-render-vulkan/shaders/triangle.vert.spv
|
||||||
|
spirv-val adapters/fparkan-render-vulkan/shaders/triangle.frag.spv
|
||||||
Команда запуска:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo run -p fparkan-vulkan-smoke --locked -- \
|
|
||||||
--out target/fparkan/native-smoke/local.json \
|
|
||||||
--timeout-seconds 120
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Поддерживается только Windows. Перед запуском убедитесь, что установлен Vulkan runtime
|
## Книга и исходники
|
||||||
от GPU vendor или LunarG Vulkan SDK и активный runtime предоставляет
|
|
||||||
`VK_LAYER_KHRONOS_validation`. Linux, macOS/MoltenVK и их smoke-пути не входят в
|
|
||||||
проектный scope.
|
|
||||||
|
|
||||||
Для полного локального closure gate используйте:
|
[Книга об устройстве движка](docs/index.md) объясняет форматы, математику,
|
||||||
|
загрузку мира и связь подсистем. Её можно читать прямо в репозитории;
|
||||||
|
`mkdocs.yml` задаёт представление на [сайте проекта](https://fparkan.popov.link).
|
||||||
|
|
||||||
```bash
|
`crates/` содержит форматы и игровые алгоритмы, `adapters/` — окно и Vulkan,
|
||||||
cargo xtask ci
|
`apps/fparkan-game` — исполняемое приложение.
|
||||||
```
|
|
||||||
|
|
||||||
Windows native smoke — единственный platform acceptance path. Вопросы hosted CI
|
|
||||||
и других операционных систем намеренно находятся вне scope разработки.
|
|
||||||
|
|
||||||
## Contributing & Support
|
|
||||||
|
|
||||||
Проект активно поддерживается и открыт для contribution. Issues и pull requests можно создавать в обоих репозиториях:
|
|
||||||
|
|
||||||
- **Primary development**: [valentineus/fparkan](https://code.popov.link/valentineus/fparkan)
|
|
||||||
- **GitHub mirror**: [valentineus/fparkan](https://github.com/valentineus/fparkan)
|
|
||||||
|
|
||||||
Основная разработка ведётся в self-hosted репозитории.
|
|
||||||
|
|
||||||
## Лицензия
|
## Лицензия
|
||||||
|
|
||||||
Проект распространяется под лицензией **[GNU GPL v2](LICENSE.txt)**.
|
[GNU GPL v2](LICENSE.txt).
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fparkan-platform = { path = "../../crates/fparkan-platform", version = "0.1.0" }
|
fparkan-platform = { path = "../../crates/fparkan-platform" }
|
||||||
raw-window-handle = "0.6"
|
raw-window-handle = "0.6"
|
||||||
winit = { version = "0.30", default-features = false, features = ["rwh_06"] }
|
winit = { version = "0.30", default-features = false, features = ["rwh_06"] }
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Minimal `winit`-backed platform adapter shim.
|
//! Minimal `winit`-backed platform adapter shim.
|
||||||
|
|
||||||
use fparkan_platform::{
|
use fparkan_platform::{
|
||||||
|
|||||||
@@ -4,35 +4,13 @@ version.workspace = true
|
|||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
build = "build.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ash = "0.38"
|
ash = "0.38"
|
||||||
ash-window = "0.13"
|
ash-window = "0.13"
|
||||||
fparkan-animation = { path = "../../crates/fparkan-animation", version = "0.1.0" }
|
fparkan-animation = { path = "../../crates/fparkan-animation" }
|
||||||
fparkan-binary = { path = "../../crates/fparkan-binary", version = "0.1.0" }
|
fparkan-binary = { path = "../../crates/fparkan-binary" }
|
||||||
fparkan-msh = { path = "../../crates/fparkan-msh", version = "0.1.0" }
|
fparkan-msh = { path = "../../crates/fparkan-msh" }
|
||||||
fparkan-platform = { path = "../../crates/fparkan-platform", version = "0.1.0" }
|
fparkan-platform = { path = "../../crates/fparkan-platform" }
|
||||||
fparkan-render = { path = "../../crates/fparkan-render", version = "0.1.0" }
|
fparkan-render = { path = "../../crates/fparkan-render" }
|
||||||
fparkan-terrain-format = { path = "../../crates/fparkan-terrain-format", version = "0.1.0" }
|
fparkan-terrain-format = { path = "../../crates/fparkan-terrain-format" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
|
|
||||||
[lints.rust]
|
|
||||||
unsafe_code = "allow"
|
|
||||||
missing_docs = "warn"
|
|
||||||
unreachable_pub = "warn"
|
|
||||||
unused_must_use = "deny"
|
|
||||||
|
|
||||||
[lints.clippy]
|
|
||||||
all = { level = "deny", priority = -1 }
|
|
||||||
pedantic = { level = "warn", priority = -1 }
|
|
||||||
unwrap_used = "deny"
|
|
||||||
expect_used = "deny"
|
|
||||||
panic = "deny"
|
|
||||||
todo = "deny"
|
|
||||||
unimplemented = "deny"
|
|
||||||
dbg_macro = "deny"
|
|
||||||
print_stdout = "warn"
|
|
||||||
print_stderr = "warn"
|
|
||||||
lossy_float_literal = "deny"
|
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
//! Build-time shader tool metadata for Vulkan reports.
|
|
||||||
|
|
||||||
use std::env;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
const SHADER_COMPILER_NAME: &str = "glslangValidator";
|
|
||||||
const SPIRV_VALIDATOR_NAME: &str = "spirv-val";
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
println!("cargo:rerun-if-env-changed=PATH");
|
|
||||||
println!("cargo:rerun-if-env-changed=FPARKAN_GLSLANG_VALIDATOR");
|
|
||||||
println!("cargo:rerun-if-env-changed=FPARKAN_SPIRV_VAL");
|
|
||||||
|
|
||||||
emit_tool_metadata(
|
|
||||||
"FPARKAN_BUILD_SHADER_COMPILER",
|
|
||||||
&tool_path("FPARKAN_GLSLANG_VALIDATOR", SHADER_COMPILER_NAME),
|
|
||||||
);
|
|
||||||
emit_tool_metadata(
|
|
||||||
"FPARKAN_BUILD_SPIRV_VALIDATOR",
|
|
||||||
&tool_path("FPARKAN_SPIRV_VAL", SPIRV_VALIDATOR_NAME),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tool_path(env_var: &str, fallback: &str) -> String {
|
|
||||||
env::var(env_var).unwrap_or_else(|_| fallback.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_tool_metadata(prefix: &str, tool: &str) {
|
|
||||||
let Some(path) = resolve_tool(tool) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
println!("cargo:rerun-if-changed={}", path.display());
|
|
||||||
let Some(version) = tool_version(&path) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(binary_sha256) = tool_sha256(&path) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let name = path
|
|
||||||
.file_name()
|
|
||||||
.and_then(|value| value.to_str())
|
|
||||||
.unwrap_or(tool)
|
|
||||||
.to_string();
|
|
||||||
println!("cargo:rustc-env={prefix}_NAME={name}");
|
|
||||||
println!("cargo:rustc-env={prefix}_VERSION={version}");
|
|
||||||
println!("cargo:rustc-env={prefix}_SHA256={binary_sha256}");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_tool(tool: &str) -> Option<std::path::PathBuf> {
|
|
||||||
let candidate = Path::new(tool);
|
|
||||||
if candidate.components().count() > 1 {
|
|
||||||
return candidate.is_file().then(|| candidate.to_path_buf());
|
|
||||||
}
|
|
||||||
let output = Command::new("which").arg(tool).output().ok()?;
|
|
||||||
if !output.status.success() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let path = String::from_utf8(output.stdout).ok()?;
|
|
||||||
let path = path.trim();
|
|
||||||
(!path.is_empty()).then(|| path.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tool_version(path: &Path) -> Option<String> {
|
|
||||||
let output = Command::new(path).arg("--version").output().ok()?;
|
|
||||||
if !output.status.success() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let stdout = String::from_utf8(output.stdout).ok()?;
|
|
||||||
stdout
|
|
||||||
.lines()
|
|
||||||
.find(|line| !line.trim().is_empty())
|
|
||||||
.map(str::trim)
|
|
||||||
.map(|line| {
|
|
||||||
line.strip_prefix("Glslang Version: ")
|
|
||||||
.unwrap_or(line)
|
|
||||||
.to_string()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tool_sha256(path: &Path) -> Option<String> {
|
|
||||||
let output = Command::new("shasum")
|
|
||||||
.args(["-a", "256"])
|
|
||||||
.arg(path)
|
|
||||||
.output()
|
|
||||||
.ok()?;
|
|
||||||
if !output.status.success() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let stdout = String::from_utf8(output.stdout).ok()?;
|
|
||||||
stdout.split_whitespace().next().map(ToString::to_string)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"schema":2,"target_env":"vulkan1.1","compiler":{"name":"glslangValidator","version":"11:16.3.0","binary_sha256":"9bcd69d830b350aaa6e2254915ff74e46070e217b67f38daad27c1fc1f22910f"},"validator":{"name":"spirv-val","version":"SPIRV-Tools v2026.2 unknown hash, 2026-04-29T17:02:58+00:00","binary_sha256":"f6d5b96ff19f073f3af0c0bcfa0c18702d288d3ec598efc242d01cd104d8354f"},"modules":[{"name":"triangle.vert","stage":"vertex","entry_point":"main","source_path":"adapters/fparkan-render-vulkan/shaders/triangle.vert","source_sha256":"fe3721202477220d2d9677b6572d7f3baecb374e94f04eea9d7286ed5e98b6c9","spirv_path":"adapters/fparkan-render-vulkan/shaders/triangle.vert.spv","word_count":358,"sha256":"4e2051ac43b933a57e5557e596bcc95952b4efbfcb45315180f89581e094398d","descriptor_sets":0,"push_constant_bytes":64,"compile_command":"glslangValidator -V --target-env vulkan1.1 -S vert -e main adapters/fparkan-render-vulkan/shaders/triangle.vert -o adapters/fparkan-render-vulkan/shaders/triangle.vert.spv","validate_command":"spirv-val --target-env vulkan1.1 adapters/fparkan-render-vulkan/shaders/triangle.vert.spv","interface_hash":"f76cc92b9aaa2261eda33bd320503e1be100a2dd7e1a9fcfe13351678dda2a06"},{"name":"triangle.frag","stage":"fragment","entry_point":"main","source_path":"adapters/fparkan-render-vulkan/shaders/triangle.frag","source_sha256":"fb3ed435af48e4fb4a817f160b467f7a561c7c547c87b6f0c1e89f5f58af7329","spirv_path":"adapters/fparkan-render-vulkan/shaders/triangle.frag.spv","word_count":296,"sha256":"8ab7bf835e166892b04b10fa1100258daf355d2f693ad311d014dbee22de5c7b","descriptor_sets":1,"push_constant_bytes":68,"compile_command":"glslangValidator -V --target-env vulkan1.1 -S frag -e main adapters/fparkan-render-vulkan/shaders/triangle.frag -o adapters/fparkan-render-vulkan/shaders/triangle.frag.spv","validate_command":"spirv-val --target-env vulkan1.1 adapters/fparkan-render-vulkan/shaders/triangle.frag.spv","interface_hash":"91552fce09a408a2baf8a608bc642b6632372d3d2316217051384159ba14cee2"}],"manifest_hash":"cf471972978ddb5c8919711ad64c93d0d64e18b1f590a89ce014f1e2a743cfb5"}
|
|
||||||
@@ -439,7 +439,6 @@ fn project_msh_to_static_mesh_in_world_space_with_node_poses(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn append_node_fallback_batch(
|
fn append_node_fallback_batch(
|
||||||
model: &ModelAsset,
|
model: &ModelAsset,
|
||||||
batch: &fparkan_msh::Batch,
|
batch: &fparkan_msh::Batch,
|
||||||
|
|||||||
@@ -1,22 +1,3 @@
|
|||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
#![deny(unsafe_op_in_unsafe_fn)]
|
#![deny(unsafe_op_in_unsafe_fn)]
|
||||||
//! Vulkan adapter facade and migration-ready backend surface contract.
|
//! Vulkan adapter facade and migration-ready backend surface contract.
|
||||||
//!
|
//!
|
||||||
@@ -57,9 +38,8 @@ pub use self::capabilities::{
|
|||||||
};
|
};
|
||||||
pub use self::instance::{
|
pub use self::instance::{
|
||||||
create_vulkan_instance_probe, plan_vulkan_instance, probe_vulkan_loader,
|
create_vulkan_instance_probe, plan_vulkan_instance, probe_vulkan_loader,
|
||||||
render_instance_plan_json, render_loader_probe_report_json, vulkan_entry_symbol_name,
|
vulkan_entry_symbol_name, VulkanInstanceConfig, VulkanInstanceError, VulkanInstancePlan,
|
||||||
VulkanInstanceConfig, VulkanInstanceError, VulkanInstancePlan, VulkanInstanceProbe,
|
VulkanInstanceProbe, VulkanLoaderError, VulkanLoaderProbeReport,
|
||||||
VulkanLoaderError, VulkanLoaderProbeReport,
|
|
||||||
};
|
};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use self::instance::{cstring_vec, ensure_instance_extensions_available};
|
use self::instance::{cstring_vec, ensure_instance_extensions_available};
|
||||||
@@ -84,8 +64,8 @@ pub use self::smoke_types::{
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use self::surface::extension_name;
|
use self::surface::extension_name;
|
||||||
pub use self::surface::{
|
pub use self::surface::{
|
||||||
create_vulkan_surface_probe, plan_vulkan_surface, render_surface_plan_json, VulkanSurfaceError,
|
create_vulkan_surface_probe, plan_vulkan_surface, VulkanSurfaceError, VulkanSurfacePlan,
|
||||||
VulkanSurfacePlan, VulkanSurfaceProbe,
|
VulkanSurfaceProbe,
|
||||||
};
|
};
|
||||||
pub use self::swapchain::{
|
pub use self::swapchain::{
|
||||||
create_vulkan_swapchain_probe, create_vulkan_swapchain_probe_for_extent, VulkanSwapchainProbe,
|
create_vulkan_swapchain_probe, create_vulkan_swapchain_probe_for_extent, VulkanSwapchainProbe,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#![allow(unsafe_code)]
|
#![allow(unsafe_code)]
|
||||||
|
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
use serde::Serialize;
|
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::ffi::{CStr, CString};
|
use std::ffi::{CStr, CString};
|
||||||
use std::os::raw::c_char;
|
use std::os::raw::c_char;
|
||||||
@@ -10,7 +9,6 @@ use super::{
|
|||||||
EXT_DEBUG_UTILS_EXTENSION, KHR_PORTABILITY_ENUMERATION_EXTENSION, MIN_VULKAN_API_VERSION,
|
EXT_DEBUG_UTILS_EXTENSION, KHR_PORTABILITY_ENUMERATION_EXTENSION, MIN_VULKAN_API_VERSION,
|
||||||
VALIDATION_LAYER_NAME,
|
VALIDATION_LAYER_NAME,
|
||||||
};
|
};
|
||||||
use crate::policy::{format_api_version, serialize_json_or_fallback};
|
|
||||||
|
|
||||||
/// Vulkan instance bootstrap configuration.
|
/// Vulkan instance bootstrap configuration.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
@@ -236,28 +234,6 @@ pub fn create_vulkan_instance_probe(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders a deterministic JSON Vulkan instance plan.
|
|
||||||
#[must_use]
|
|
||||||
pub fn render_instance_plan_json(plan: &VulkanInstancePlan) -> String {
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct InstancePlanJson<'a> {
|
|
||||||
schema: u32,
|
|
||||||
create_flags: u32,
|
|
||||||
validation_requested: bool,
|
|
||||||
enabled_extensions: &'a [String],
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize_json_or_fallback(
|
|
||||||
&InstancePlanJson {
|
|
||||||
schema: plan.schema,
|
|
||||||
create_flags: plan.create_flags,
|
|
||||||
validation_requested: plan.validation_requested,
|
|
||||||
enabled_extensions: &plan.enabled_extensions,
|
|
||||||
},
|
|
||||||
"{\"schema\":0,\"create_flags\":0,\"validation_requested\":false,\"enabled_extensions\":[]}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Opens the Vulkan loader and reports the supported instance API version.
|
/// Opens the Vulkan loader and reports the supported instance API version.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
@@ -288,26 +264,6 @@ pub fn vulkan_entry_symbol_name() -> &'static CStr {
|
|||||||
c"vkGetInstanceProcAddr"
|
c"vkGetInstanceProcAddr"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders a deterministic JSON Vulkan loader report.
|
|
||||||
#[must_use]
|
|
||||||
pub fn render_loader_probe_report_json(report: &VulkanLoaderProbeReport) -> String {
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct LoaderProbeReportJson {
|
|
||||||
schema: u32,
|
|
||||||
loader_available: bool,
|
|
||||||
instance_api: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize_json_or_fallback(
|
|
||||||
&LoaderProbeReportJson {
|
|
||||||
schema: report.schema,
|
|
||||||
loader_available: report.loader_available,
|
|
||||||
instance_api: format_api_version(report.instance_api_version),
|
|
||||||
},
|
|
||||||
"{\"schema\":0,\"loader_available\":false,\"instance_api\":\"0.0.0\"}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn available_instance_extensions(entry: &ash::Entry) -> Result<Vec<String>, VulkanInstanceError> {
|
fn available_instance_extensions(entry: &ash::Entry) -> Result<Vec<String>, VulkanInstanceError> {
|
||||||
let available_extensions =
|
let available_extensions =
|
||||||
// SAFETY: Enumerating instance extensions reads loader-owned immutable metadata.
|
// SAFETY: Enumerating instance extensions reads loader-owned immutable metadata.
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ pub(super) struct VulkanDepthAttachment {
|
|||||||
pub(super) format: vk::Format,
|
pub(super) format: vk::Format,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
pub(super) fn create_depth_attachment(
|
pub(super) fn create_depth_attachment(
|
||||||
instance: &VulkanInstanceProbe,
|
instance: &VulkanInstanceProbe,
|
||||||
device: &VulkanLogicalDeviceProbe,
|
device: &VulkanLogicalDeviceProbe,
|
||||||
@@ -249,7 +248,6 @@ pub(super) fn create_static_mesh_index_buffer(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
pub(super) fn create_static_texture_image(
|
pub(super) fn create_static_texture_image(
|
||||||
instance: &VulkanInstanceProbe,
|
instance: &VulkanInstanceProbe,
|
||||||
device: &VulkanLogicalDeviceProbe,
|
device: &VulkanLogicalDeviceProbe,
|
||||||
@@ -376,7 +374,6 @@ pub(super) fn create_static_texture_image(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
fn upload_static_texture(
|
fn upload_static_texture(
|
||||||
device: &VulkanLogicalDeviceProbe,
|
device: &VulkanLogicalDeviceProbe,
|
||||||
command_pool: vk::CommandPool,
|
command_pool: vk::CommandPool,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use super::{
|
|||||||
VulkanValidationReport,
|
VulkanValidationReport,
|
||||||
};
|
};
|
||||||
use crate::policy::KHR_PORTABILITY_SUBSET_EXTENSION;
|
use crate::policy::KHR_PORTABILITY_SUBSET_EXTENSION;
|
||||||
use crate::shader_manifest::{triangle_shader_manifest, validate_shader_manifest};
|
use crate::shader::validate_triangle_shaders;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn take_runtime_owners_in_dependency_order<Instance, Validation, Surface, Device, Swapchain>(
|
fn take_runtime_owners_in_dependency_order<Instance, Validation, Surface, Device, Swapchain>(
|
||||||
@@ -103,7 +103,7 @@ impl VulkanSmokeRenderer {
|
|||||||
///
|
///
|
||||||
/// Returns [`VulkanSmokeRendererError`] when Vulkan bootstrap, pipeline creation,
|
/// Returns [`VulkanSmokeRendererError`] when Vulkan bootstrap, pipeline creation,
|
||||||
/// memory allocation, or synchronization resource creation fails.
|
/// memory allocation, or synchronization resource creation fails.
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
create_info: &VulkanSmokeRendererCreateInfo,
|
create_info: &VulkanSmokeRendererCreateInfo,
|
||||||
) -> Result<Self, VulkanSmokeRendererError> {
|
) -> Result<Self, VulkanSmokeRendererError> {
|
||||||
@@ -120,8 +120,7 @@ impl VulkanSmokeRenderer {
|
|||||||
resolve_draw_texture_indices(&create_info.mesh.draw_ranges, &create_info.materials)
|
resolve_draw_texture_indices(&create_info.mesh.draw_ranges, &create_info.materials)
|
||||||
.map_err(|context| VulkanSmokeRendererError::InvalidStaticMesh { context })?;
|
.map_err(|context| VulkanSmokeRendererError::InvalidStaticMesh { context })?;
|
||||||
let bootstrap_progress = create_info.bootstrap_progress.as_ref();
|
let bootstrap_progress = create_info.bootstrap_progress.as_ref();
|
||||||
let shader_manifest = validate_shader_manifest(&triangle_shader_manifest())
|
validate_triangle_shaders().map_err(VulkanSmokeRendererError::Shader)?;
|
||||||
.map_err(VulkanSmokeRendererError::ShaderManifest)?;
|
|
||||||
let surface_plan = plan_vulkan_surface(Some(create_info.native_handles))
|
let surface_plan = plan_vulkan_surface(Some(create_info.native_handles))
|
||||||
.map_err(VulkanSmokeRendererError::Surface)?;
|
.map_err(VulkanSmokeRendererError::Surface)?;
|
||||||
let mut instance_config = VulkanInstanceConfig::smoke(&create_info.application_name);
|
let mut instance_config = VulkanInstanceConfig::smoke(&create_info.application_name);
|
||||||
@@ -240,7 +239,6 @@ impl VulkanSmokeRenderer {
|
|||||||
pending_extent: None,
|
pending_extent: None,
|
||||||
swapchain_recreate_count: 0,
|
swapchain_recreate_count: 0,
|
||||||
report: VulkanSmokeRendererReport {
|
report: VulkanSmokeRendererReport {
|
||||||
shader_manifest_hash: shader_manifest.manifest_hash.clone(),
|
|
||||||
portability_enumeration: instance_config.enable_portability_enumeration,
|
portability_enumeration: instance_config.enable_portability_enumeration,
|
||||||
portability_subset_enabled: false,
|
portability_subset_enabled: false,
|
||||||
device_name: String::new(),
|
device_name: String::new(),
|
||||||
@@ -260,7 +258,6 @@ impl VulkanSmokeRenderer {
|
|||||||
let device_ref = renderer.device_ref()?;
|
let device_ref = renderer.device_ref()?;
|
||||||
let swapchain_ref = renderer.swapchain_ref()?;
|
let swapchain_ref = renderer.swapchain_ref()?;
|
||||||
renderer.report = VulkanSmokeRendererReport {
|
renderer.report = VulkanSmokeRendererReport {
|
||||||
shader_manifest_hash: shader_manifest.manifest_hash,
|
|
||||||
portability_enumeration: renderer
|
portability_enumeration: renderer
|
||||||
.instance
|
.instance
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -426,7 +423,7 @@ impl VulkanSmokeRenderer {
|
|||||||
///
|
///
|
||||||
/// Returns [`VulkanSmokeRendererError`] when synchronization, command recording,
|
/// Returns [`VulkanSmokeRendererError`] when synchronization, command recording,
|
||||||
/// submission, or presentation fails.
|
/// submission, or presentation fails.
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
pub fn draw_frame(&mut self) -> Result<VulkanSmokeFrameOutcome, VulkanSmokeRendererError> {
|
pub fn draw_frame(&mut self) -> Result<VulkanSmokeFrameOutcome, VulkanSmokeRendererError> {
|
||||||
if let Some(extent) = self.pending_extent.take() {
|
if let Some(extent) = self.pending_extent.take() {
|
||||||
if extent.0 == 0 || extent.1 == 0 {
|
if extent.0 == 0 || extent.1 == 0 {
|
||||||
@@ -632,7 +629,6 @@ impl VulkanSmokeRenderer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
fn record_command_buffer(
|
fn record_command_buffer(
|
||||||
&mut self,
|
&mut self,
|
||||||
image_index: usize,
|
image_index: usize,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use super::{
|
|||||||
VulkanSurfaceProbe, VulkanSwapchainProbe, VulkanSwapchainProbeError, VulkanSwapchainResources,
|
VulkanSurfaceProbe, VulkanSwapchainProbe, VulkanSwapchainProbeError, VulkanSwapchainResources,
|
||||||
VulkanValidationMessenger,
|
VulkanValidationMessenger,
|
||||||
};
|
};
|
||||||
use crate::shader_manifest::VulkanShaderManifestError;
|
use crate::shader::VulkanShaderError;
|
||||||
|
|
||||||
/// Creates a live native Vulkan renderer for the Stage 0 smoke loop.
|
/// Creates a live native Vulkan renderer for the Stage 0 smoke loop.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -536,7 +536,7 @@ impl VulkanSmokeBootstrapProgress {
|
|||||||
|
|
||||||
/// Stable snapshot of measured bootstrap progress.
|
/// Stable snapshot of measured bootstrap progress.
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
|
||||||
pub struct VulkanSmokeBootstrapSnapshot {
|
pub struct VulkanSmokeBootstrapSnapshot {
|
||||||
/// Whether the Vulkan loader was resolved.
|
/// Whether the Vulkan loader was resolved.
|
||||||
pub loader_available: bool,
|
pub loader_available: bool,
|
||||||
@@ -562,8 +562,6 @@ const BOOTSTRAP_SWAPCHAIN_CREATED: u8 = 1 << 5;
|
|||||||
/// Stable smoke renderer bootstrap report.
|
/// Stable smoke renderer bootstrap report.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct VulkanSmokeRendererReport {
|
pub struct VulkanSmokeRendererReport {
|
||||||
/// Checked-in shader manifest hash used by the renderer.
|
|
||||||
pub shader_manifest_hash: String,
|
|
||||||
/// Whether portability enumeration was enabled at instance creation.
|
/// Whether portability enumeration was enabled at instance creation.
|
||||||
pub portability_enumeration: bool,
|
pub portability_enumeration: bool,
|
||||||
/// Whether the logical device enabled `VK_KHR_portability_subset`.
|
/// Whether the logical device enabled `VK_KHR_portability_subset`.
|
||||||
@@ -652,8 +650,8 @@ pub enum VulkanSmokeRendererError {
|
|||||||
LogicalDevice(VulkanLogicalDeviceError),
|
LogicalDevice(VulkanLogicalDeviceError),
|
||||||
/// Swapchain bootstrap failed.
|
/// Swapchain bootstrap failed.
|
||||||
Swapchain(VulkanSwapchainProbeError),
|
Swapchain(VulkanSwapchainProbeError),
|
||||||
/// Shader manifest validation failed.
|
/// Checked-in shader validation failed.
|
||||||
ShaderManifest(VulkanShaderManifestError),
|
Shader(VulkanShaderError),
|
||||||
/// Vulkan operation failed.
|
/// Vulkan operation failed.
|
||||||
VulkanOperation {
|
VulkanOperation {
|
||||||
/// Operation context.
|
/// Operation context.
|
||||||
@@ -695,7 +693,7 @@ impl std::fmt::Display for VulkanSmokeRendererError {
|
|||||||
Self::Surface(error) => write!(f, "{error}"),
|
Self::Surface(error) => write!(f, "{error}"),
|
||||||
Self::LogicalDevice(error) => write!(f, "{error}"),
|
Self::LogicalDevice(error) => write!(f, "{error}"),
|
||||||
Self::Swapchain(error) => write!(f, "{error}"),
|
Self::Swapchain(error) => write!(f, "{error}"),
|
||||||
Self::ShaderManifest(error) => write!(f, "{error}"),
|
Self::Shader(error) => write!(f, "{error}"),
|
||||||
Self::VulkanOperation { context, result } => {
|
Self::VulkanOperation { context, result } => {
|
||||||
write!(f, "{context}: {result:?}")
|
write!(f, "{context}: {result:?}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,10 @@
|
|||||||
|
|
||||||
use ash::{khr::surface, vk};
|
use ash::{khr::surface, vk};
|
||||||
use fparkan_platform::NativeWindowHandles;
|
use fparkan_platform::NativeWindowHandles;
|
||||||
use serde::Serialize;
|
|
||||||
use std::ffi::CStr;
|
use std::ffi::CStr;
|
||||||
use std::os::raw::c_char;
|
use std::os::raw::c_char;
|
||||||
|
|
||||||
use super::VulkanInstanceProbe;
|
use super::VulkanInstanceProbe;
|
||||||
use crate::policy::serialize_json_or_fallback;
|
|
||||||
|
|
||||||
/// Deterministic Vulkan surface creation plan.
|
/// Deterministic Vulkan surface creation plan.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
@@ -132,24 +130,6 @@ pub fn create_vulkan_surface_probe(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders a deterministic JSON Vulkan surface plan.
|
|
||||||
#[must_use]
|
|
||||||
pub fn render_surface_plan_json(plan: &VulkanSurfacePlan) -> String {
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct SurfacePlanJson<'a> {
|
|
||||||
schema: u32,
|
|
||||||
required_instance_extensions: &'a [String],
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize_json_or_fallback(
|
|
||||||
&SurfacePlanJson {
|
|
||||||
schema: plan.schema,
|
|
||||||
required_instance_extensions: &plan.required_instance_extensions,
|
|
||||||
},
|
|
||||||
"{\"schema\":0,\"required_instance_extensions\":[]}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn extension_name(extension: *const c_char) -> Result<String, VulkanSurfaceError> {
|
pub(super) fn extension_name(extension: *const c_char) -> Result<String, VulkanSurfaceError> {
|
||||||
// SAFETY: `ash-window` returns extension pointers to static NUL-terminated Vulkan names.
|
// SAFETY: `ash-window` returns extension pointers to static NUL-terminated Vulkan names.
|
||||||
let name = unsafe { CStr::from_ptr(extension) };
|
let name = unsafe { CStr::from_ptr(extension) };
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ struct PartialSwapchainResources {
|
|||||||
command_buffers: Vec<vk::CommandBuffer>,
|
command_buffers: Vec<vk::CommandBuffer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
|
|
||||||
pub(super) fn create_swapchain_resources(
|
pub(super) fn create_swapchain_resources(
|
||||||
instance: &VulkanInstanceProbe,
|
instance: &VulkanInstanceProbe,
|
||||||
device: &VulkanLogicalDeviceProbe,
|
device: &VulkanLogicalDeviceProbe,
|
||||||
@@ -205,7 +204,6 @@ fn create_swapchain_image_views(
|
|||||||
Ok(image_views)
|
Ok(image_views)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
fn create_swapchain_pipeline_bundle(
|
fn create_swapchain_pipeline_bundle(
|
||||||
instance: &VulkanInstanceProbe,
|
instance: &VulkanInstanceProbe,
|
||||||
device: &VulkanLogicalDeviceProbe,
|
device: &VulkanLogicalDeviceProbe,
|
||||||
@@ -433,7 +431,6 @@ fn create_pipeline_layout(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
fn create_texture_descriptor_bundle(
|
fn create_texture_descriptor_bundle(
|
||||||
device: &VulkanLogicalDeviceProbe,
|
device: &VulkanLogicalDeviceProbe,
|
||||||
textures: &[VulkanAllocatedImage],
|
textures: &[VulkanAllocatedImage],
|
||||||
@@ -554,12 +551,10 @@ fn create_texture_descriptor_bundle(
|
|||||||
Ok((layout, pool, descriptor_sets, sampler))
|
Ok((layout, pool, descriptor_sets, sampler))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
|
||||||
fn extent_component_to_f32(value: u32) -> f32 {
|
fn extent_component_to_f32(value: u32) -> f32 {
|
||||||
value as f32
|
value as f32
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
fn create_graphics_pipeline_cache(
|
fn create_graphics_pipeline_cache(
|
||||||
device: &VulkanLogicalDeviceProbe,
|
device: &VulkanLogicalDeviceProbe,
|
||||||
render_pass: vk::RenderPass,
|
render_pass: vk::RenderPass,
|
||||||
@@ -596,7 +591,6 @@ fn create_graphics_pipeline_cache(
|
|||||||
Ok(pipelines)
|
Ok(pipelines)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
fn create_graphics_pipeline(
|
fn create_graphics_pipeline(
|
||||||
device: &VulkanLogicalDeviceProbe,
|
device: &VulkanLogicalDeviceProbe,
|
||||||
render_pass: vk::RenderPass,
|
render_pass: vk::RenderPass,
|
||||||
|
|||||||
@@ -1,114 +1,9 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::ffi::{SPIRV_MAGIC, SPIRV_VERSION_1_0};
|
||||||
use crate::policy::{KHR_PORTABILITY_SUBSET_EXTENSION, KHR_SWAPCHAIN_EXTENSION};
|
use crate::policy::{KHR_PORTABILITY_SUBSET_EXTENSION, KHR_SWAPCHAIN_EXTENSION};
|
||||||
use crate::shader_manifest::{
|
use crate::shader::{validate_spirv_container, validate_triangle_shaders, VulkanShaderError};
|
||||||
SHADER_COMPILER_BINARY_SHA256, SHADER_COMPILER_NAME, SHADER_COMPILER_VERSION,
|
|
||||||
SHADER_MANIFEST_SCHEMA, SHADER_TARGET_ENV, SPIRV_MAGIC, SPIRV_VALIDATOR_BINARY_SHA256,
|
|
||||||
SPIRV_VALIDATOR_NAME, SPIRV_VALIDATOR_VERSION, SPIRV_VERSION_1_0,
|
|
||||||
TRIANGLE_VERTEX_COMPILE_COMMAND, TRIANGLE_VERTEX_SOURCE_PATH, TRIANGLE_VERTEX_SOURCE_SHA256,
|
|
||||||
TRIANGLE_VERTEX_SPIRV_PATH, TRIANGLE_VERTEX_VALIDATE_COMMAND,
|
|
||||||
};
|
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use fparkan_platform::{DepthStencilSupport, RenderRequest};
|
use fparkan_platform::{DepthStencilSupport, RenderRequest};
|
||||||
use fparkan_render::{
|
|
||||||
DrawCommand, DrawId, GpuMaterialId, GpuMeshId, IndexRange, LegacyPipelineState, RenderCommand,
|
|
||||||
RenderPhase,
|
|
||||||
};
|
|
||||||
use fparkan_render::{RenderBackend, RenderError};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn planning_backend_tracks_render_request_and_simulated_present() -> Result<(), RenderError> {
|
|
||||||
let mut backend = VulkanPlanningBackend::new();
|
|
||||||
let request = RenderRequest {
|
|
||||||
presentation: fparkan_platform::PresentationMode::Immediate,
|
|
||||||
..RenderRequest::conservative()
|
|
||||||
};
|
|
||||||
backend.set_render_request(request);
|
|
||||||
assert_eq!(backend.render_request(), request);
|
|
||||||
assert_eq!(backend.report().request.current_request, request);
|
|
||||||
assert_eq!(backend.report().request.request_updates, 1);
|
|
||||||
|
|
||||||
let commands = fparkan_render::RenderCommandList {
|
|
||||||
commands: vec![
|
|
||||||
RenderCommand::BeginFrame,
|
|
||||||
RenderCommand::Draw(DrawCommand {
|
|
||||||
id: DrawId(11),
|
|
||||||
phase: RenderPhase::Opaque,
|
|
||||||
object_id: None,
|
|
||||||
mesh: GpuMeshId(1),
|
|
||||||
material: GpuMaterialId(2),
|
|
||||||
pipeline_key: LegacyPipelineState::default().into(),
|
|
||||||
transform: [1.0; 16],
|
|
||||||
range: IndexRange { start: 0, count: 3 },
|
|
||||||
stable_order: 7,
|
|
||||||
}),
|
|
||||||
RenderCommand::EndFrame,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
backend.execute(&commands)?;
|
|
||||||
assert_eq!(backend.state(), VulkanPlanningBackendState::Configured);
|
|
||||||
assert_eq!(backend.report().execution.planned_frames, 1);
|
|
||||||
assert_eq!(backend.report().execution.submission_plans, 1);
|
|
||||||
assert_eq!(backend.report().execution.simulated_presents, 1);
|
|
||||||
assert!(backend.report().execution.last_capture_size > 0);
|
|
||||||
assert_eq!(
|
|
||||||
backend.report().last_frame_submission,
|
|
||||||
Some(VulkanFrameSubmissionPlan {
|
|
||||||
schema: 1,
|
|
||||||
frames_in_flight: 2,
|
|
||||||
command_buffers: 2,
|
|
||||||
semaphores_per_frame: 2,
|
|
||||||
fences_per_frame: 1,
|
|
||||||
draw_count: 1,
|
|
||||||
indexed_vertex_count: 3,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn frame_submission_plan_json_is_stable() -> Result<(), RenderError> {
|
|
||||||
let commands = fparkan_render::RenderCommandList {
|
|
||||||
commands: vec![
|
|
||||||
RenderCommand::BeginFrame,
|
|
||||||
RenderCommand::Draw(DrawCommand {
|
|
||||||
id: DrawId(11),
|
|
||||||
phase: RenderPhase::Opaque,
|
|
||||||
object_id: None,
|
|
||||||
mesh: GpuMeshId(1),
|
|
||||||
material: GpuMaterialId(2),
|
|
||||||
pipeline_key: LegacyPipelineState::default().into(),
|
|
||||||
transform: [1.0; 16],
|
|
||||||
range: IndexRange { start: 0, count: 3 },
|
|
||||||
stable_order: 7,
|
|
||||||
}),
|
|
||||||
RenderCommand::EndFrame,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
let swapchain = VulkanSwapchainPlan {
|
|
||||||
schema: 1,
|
|
||||||
extent: (1, 1),
|
|
||||||
format: VulkanSurfaceFormat {
|
|
||||||
format: vk::Format::B8G8R8A8_SRGB.as_raw(),
|
|
||||||
color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
|
|
||||||
},
|
|
||||||
present_mode: vk::PresentModeKHR::FIFO.as_raw(),
|
|
||||||
image_count: 3,
|
|
||||||
image_usage: vk::ImageUsageFlags::COLOR_ATTACHMENT.as_raw(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let plan = plan_vulkan_frame_submission(&swapchain, &commands)?;
|
|
||||||
|
|
||||||
assert_eq!(plan.frames_in_flight, 2);
|
|
||||||
assert_eq!(plan.command_buffers, 3);
|
|
||||||
assert_eq!(plan.draw_count, 1);
|
|
||||||
assert_eq!(plan.indexed_vertex_count, 3);
|
|
||||||
assert_eq!(
|
|
||||||
render_frame_submission_plan_json(&plan),
|
|
||||||
"{\"schema\":1,\"frames_in_flight\":2,\"command_buffers\":3,\"semaphores_per_frame\":2,\"fences_per_frame\":1,\"draw_count\":1,\"indexed_vertex_count\":3}"
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn device_scoring_is_deterministic_and_prefers_discrete_unified_queue() {
|
fn device_scoring_is_deterministic_and_prefers_discrete_unified_queue() {
|
||||||
@@ -390,38 +285,6 @@ fn capability_report_preserves_informational_sampled_formats_and_limits() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn capability_report_json_is_stable() {
|
|
||||||
let mut rejected = device("Rejected", VulkanDeviceType::IntegratedGpu, 0, true, false);
|
|
||||||
rejected.present_modes.clear();
|
|
||||||
let report = select_physical_device(&[
|
|
||||||
rejected,
|
|
||||||
device("GPU \"A\"", VulkanDeviceType::DiscreteGpu, 3, true, false),
|
|
||||||
])
|
|
||||||
.expect("selected device");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
render_capability_report_json(&report),
|
|
||||||
"{\"schema\":1,\"vulkan_api\":\"1.1.0\",\"device_name\":\"GPU \\\"A\\\"\",\"score\":1101,\"graphics_queue_family\":3,\"present_queue_family\":3,\"portability_subset\":false,\"enabled_extensions\":[\"VK_KHR_swapchain\"],\"informational_capabilities\":{\"sampled_color_formats\":[50],\"sampled_depth_formats\":[126],\"limits\":{\"max_image_dimension_2d\":4096,\"max_sampler_allocation_count\":4096,\"max_per_stage_descriptor_samplers\":16,\"max_bound_descriptor_sets\":4}},\"rejected_devices\":[{\"device_name\":\"Rejected\",\"reason_code\":\"missing_present_mode\",\"reason\":\"Vulkan device Rejected has no supported present mode\"}]}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn loader_probe_report_json_is_stable() {
|
|
||||||
assert_eq!(
|
|
||||||
vulkan_entry_symbol_name().to_bytes(),
|
|
||||||
b"vkGetInstanceProcAddr"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
render_loader_probe_report_json(&VulkanLoaderProbeReport {
|
|
||||||
schema: 1,
|
|
||||||
loader_available: true,
|
|
||||||
instance_api_version: vk::API_VERSION_1_2,
|
|
||||||
}),
|
|
||||||
"{\"schema\":1,\"loader_available\":true,\"instance_api\":\"1.2.0\"}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn loader_error_display_is_actionable() {
|
fn loader_error_display_is_actionable() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -446,9 +309,15 @@ fn instance_plan_is_sorted_deduplicated_and_portability_aware() {
|
|||||||
enable_validation: true,
|
enable_validation: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
assert_eq!(plan.create_flags, 1);
|
||||||
|
assert!(plan.validation_requested);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
render_instance_plan_json(&plan),
|
plan.enabled_extensions,
|
||||||
"{\"schema\":1,\"create_flags\":1,\"validation_requested\":true,\"enabled_extensions\":[\"VK_EXT_debug_utils\",\"VK_KHR_portability_enumeration\",\"VK_KHR_surface\"]}"
|
vec![
|
||||||
|
"VK_EXT_debug_utils".to_string(),
|
||||||
|
KHR_PORTABILITY_ENUMERATION_EXTENSION.to_string(),
|
||||||
|
"VK_KHR_surface".to_string(),
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,20 +378,6 @@ fn surface_plan_requires_native_handles() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn surface_plan_json_is_stable() {
|
|
||||||
assert_eq!(
|
|
||||||
render_surface_plan_json(&VulkanSurfacePlan {
|
|
||||||
schema: 1,
|
|
||||||
required_instance_extensions: vec![
|
|
||||||
"VK_KHR_surface".to_string(),
|
|
||||||
"VK_EXT_metal_surface".to_string(),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
"{\"schema\":1,\"required_instance_extensions\":[\"VK_KHR_surface\",\"VK_EXT_metal_surface\"]}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn static_surface_extension_name_is_decoded() {
|
fn static_surface_extension_name_is_decoded() {
|
||||||
let name = extension_name(ash::khr::surface::NAME.as_ptr()).expect("extension name");
|
let name = extension_name(ash::khr::surface::NAME.as_ptr()).expect("extension name");
|
||||||
@@ -620,128 +475,31 @@ fn swapchain_plan_rejects_missing_surface_data_and_empty_extent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn swapchain_plan_json_and_recreation_reports_are_stable() {
|
fn checked_in_shaders_have_valid_spirv_containers() {
|
||||||
let plan = plan_vulkan_swapchain(&swapchain_request()).expect("swapchain plan");
|
assert_eq!(validate_triangle_shaders(), Ok(()));
|
||||||
assert_eq!(
|
|
||||||
render_swapchain_plan_json(&plan),
|
|
||||||
"{\"schema\":1,\"extent\":[1024,720],\"format\":50,\"color_space\":0,\"present_mode\":1,\"image_count\":3,\"image_usage\":16}"
|
|
||||||
);
|
|
||||||
|
|
||||||
let report = swapchain_recreation_report(
|
|
||||||
VulkanSwapchainRecreationReason::OutOfDate,
|
|
||||||
(1024, 720),
|
|
||||||
(1280, 720),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
render_swapchain_recreation_report_json(&report),
|
|
||||||
"{\"schema\":1,\"reason\":\"out_of_date\",\"previous_extent\":[1024,720],\"next_extent\":[1280,720]}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn triangle_shader_manifest_hashes_are_stable() {
|
fn shader_container_rejects_invalid_spirv() {
|
||||||
let report = validate_shader_manifest(&triangle_shader_manifest()).expect("shader manifest");
|
|
||||||
|
|
||||||
assert_eq!(report.schema, SHADER_MANIFEST_SCHEMA);
|
|
||||||
assert_eq!(report.target_env, SHADER_TARGET_ENV);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
report.compiler,
|
validate_spirv_container("triangle.vert", &[0xFFFF_FFFF, SPIRV_VERSION_1_0, 0, 1, 0]),
|
||||||
VulkanShaderToolManifest {
|
Err(VulkanShaderError::InvalidMagic {
|
||||||
name: SHADER_COMPILER_NAME,
|
|
||||||
version: SHADER_COMPILER_VERSION,
|
|
||||||
binary_sha256: SHADER_COMPILER_BINARY_SHA256,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
report.validator,
|
|
||||||
VulkanShaderToolManifest {
|
|
||||||
name: SPIRV_VALIDATOR_NAME,
|
|
||||||
version: SPIRV_VALIDATOR_VERSION,
|
|
||||||
binary_sha256: SPIRV_VALIDATOR_BINARY_SHA256,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert_eq!(report.modules.len(), 2);
|
|
||||||
assert_eq!(report.modules[0].name, "triangle.vert");
|
|
||||||
assert_eq!(report.modules[0].stage, VulkanShaderStage::Vertex);
|
|
||||||
assert_eq!(report.modules[0].source_path, TRIANGLE_VERTEX_SOURCE_PATH);
|
|
||||||
assert_eq!(
|
|
||||||
report.modules[0].source_sha256,
|
|
||||||
TRIANGLE_VERTEX_SOURCE_SHA256
|
|
||||||
);
|
|
||||||
assert_eq!(report.modules[0].spirv_path, TRIANGLE_VERTEX_SPIRV_PATH);
|
|
||||||
assert_eq!(report.modules[0].word_count, 358);
|
|
||||||
assert_eq!(
|
|
||||||
report.modules[0].sha256,
|
|
||||||
"4e2051ac43b933a57e5557e596bcc95952b4efbfcb45315180f89581e094398d"
|
|
||||||
);
|
|
||||||
assert_eq!(report.modules[0].descriptor_sets, 0);
|
|
||||||
assert_eq!(report.modules[0].push_constant_bytes, 64);
|
|
||||||
assert_eq!(
|
|
||||||
report.modules[0].compile_command,
|
|
||||||
TRIANGLE_VERTEX_COMPILE_COMMAND
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
report.modules[0].validate_command,
|
|
||||||
TRIANGLE_VERTEX_VALIDATE_COMMAND
|
|
||||||
);
|
|
||||||
assert!(!report.modules[0].interface_hash.is_empty());
|
|
||||||
assert_eq!(
|
|
||||||
report.modules[1].sha256,
|
|
||||||
"8ab7bf835e166892b04b10fa1100258daf355d2f693ad311d014dbee22de5c7b"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
report.manifest_hash,
|
|
||||||
"cf471972978ddb5c8919711ad64c93d0d64e18b1f590a89ce014f1e2a743cfb5"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn shader_manifest_report_json_is_stable() {
|
|
||||||
let report = validate_shader_manifest(&triangle_shader_manifest()).expect("shader manifest");
|
|
||||||
let json = render_shader_manifest_report_json(&report);
|
|
||||||
|
|
||||||
assert!(json.contains(SHADER_COMPILER_NAME));
|
|
||||||
assert!(json.contains(SPIRV_VALIDATOR_NAME));
|
|
||||||
assert!(json.contains(TRIANGLE_VERTEX_SOURCE_PATH));
|
|
||||||
assert!(json.contains(TRIANGLE_VERTEX_COMPILE_COMMAND));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn checked_in_shader_manifest_matches_generated_report() {
|
|
||||||
let report = validate_shader_manifest(&triangle_shader_manifest()).expect("shader manifest");
|
|
||||||
assert_eq!(
|
|
||||||
render_shader_manifest_report_json(&report),
|
|
||||||
include_str!("../../shaders/manifest.json").trim()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn shader_manifest_rejects_invalid_spirv_containers() {
|
|
||||||
let mut module = triangle_shader_manifest().remove(0);
|
|
||||||
module.words = &[0xFFFF_FFFF, SPIRV_VERSION_1_0, 0, 1, 0];
|
|
||||||
assert_eq!(
|
|
||||||
validate_shader_manifest(&[module]),
|
|
||||||
Err(VulkanShaderManifestError::InvalidMagic {
|
|
||||||
name: "triangle.vert",
|
name: "triangle.vert",
|
||||||
found: 0xFFFF_FFFF,
|
found: 0xFFFF_FFFF,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut module = triangle_shader_manifest().remove(0);
|
|
||||||
module.words = &[SPIRV_MAGIC, 0, 0, 1, 0];
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
validate_shader_manifest(&[module]),
|
validate_spirv_container("triangle.vert", &[SPIRV_MAGIC, 0, 0, 1, 0]),
|
||||||
Err(VulkanShaderManifestError::UnsupportedVersion {
|
Err(VulkanShaderError::UnsupportedVersion {
|
||||||
name: "triangle.vert",
|
name: "triangle.vert",
|
||||||
found: 0,
|
found: 0,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut module = triangle_shader_manifest().remove(0);
|
|
||||||
module.words = &[SPIRV_MAGIC, SPIRV_VERSION_1_0, 0, 0, 0];
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
validate_shader_manifest(&[module]),
|
validate_spirv_container("triangle.vert", &[SPIRV_MAGIC, SPIRV_VERSION_1_0, 0, 0, 0]),
|
||||||
Err(VulkanShaderManifestError::InvalidBound {
|
Err(VulkanShaderError::InvalidBound {
|
||||||
name: "triangle.vert",
|
name: "triangle.vert",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,11 +2,9 @@
|
|||||||
//! Vulkan adapter public surface.
|
//! Vulkan adapter public surface.
|
||||||
|
|
||||||
mod ffi;
|
mod ffi;
|
||||||
mod planning_backend;
|
|
||||||
mod policy;
|
mod policy;
|
||||||
mod shader_manifest;
|
mod shader;
|
||||||
|
|
||||||
pub use ffi::*;
|
pub use ffi::*;
|
||||||
pub use planning_backend::*;
|
|
||||||
pub use policy::*;
|
pub use policy::*;
|
||||||
pub use shader_manifest::*;
|
pub use shader::VulkanShaderError;
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
use ash::vk;
|
|
||||||
use fparkan_platform::RenderRequest;
|
|
||||||
use fparkan_render::{
|
|
||||||
canonical_capture, FrameOutput, RenderBackend, RenderCommandList, RenderError,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
plan_vulkan_frame_submission, VulkanFrameSubmissionPlan, VulkanSurfaceFormat,
|
|
||||||
VulkanSwapchainPlan,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Vulkan backend migration readiness.
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
||||||
pub enum VulkanPlanningBackendState {
|
|
||||||
/// Planning facade is configured and able to accept command lists.
|
|
||||||
#[default]
|
|
||||||
Configured,
|
|
||||||
/// Adapter is tracking a recoverable runtime surface/depth pipeline fault.
|
|
||||||
Degraded,
|
|
||||||
/// Adapter has encountered a non-recoverable error.
|
|
||||||
Error,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Diagnostics for planning-facade request tracking.
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct VulkanPlanningRequestReport {
|
|
||||||
/// Last render request observed by the planning facade.
|
|
||||||
pub current_request: RenderRequest,
|
|
||||||
/// Number of meaningful request updates applied to the facade.
|
|
||||||
pub request_updates: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for VulkanPlanningRequestReport {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
current_request: RenderRequest::conservative(),
|
|
||||||
request_updates: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Diagnostics for planning-facade execution telemetry.
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq)]
|
|
||||||
pub struct VulkanPlanningExecutionReport {
|
|
||||||
/// Total frames planned by the facade.
|
|
||||||
pub planned_frames: u64,
|
|
||||||
/// Total frame-submission plans emitted by the facade.
|
|
||||||
pub submission_plans: u64,
|
|
||||||
/// Last command-capture byte size.
|
|
||||||
pub last_capture_size: usize,
|
|
||||||
/// Number of simulated present calls issued by the planning facade.
|
|
||||||
pub simulated_presents: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Diagnostics for Vulkan planning backend setup and frame progression.
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq)]
|
|
||||||
pub struct VulkanPlanningBackendReport {
|
|
||||||
/// Request-tracking telemetry.
|
|
||||||
pub request: VulkanPlanningRequestReport,
|
|
||||||
/// Execution-planning telemetry.
|
|
||||||
pub execution: VulkanPlanningExecutionReport,
|
|
||||||
/// Last deterministic frame submission plan.
|
|
||||||
pub last_frame_submission: Option<VulkanFrameSubmissionPlan>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Vulkan planning backend facade used by the game entrypoint.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct VulkanPlanningBackend {
|
|
||||||
state: VulkanPlanningBackendState,
|
|
||||||
report: VulkanPlanningBackendReport,
|
|
||||||
swapchain_plan: VulkanSwapchainPlan,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for VulkanPlanningBackend {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VulkanPlanningBackend {
|
|
||||||
/// Creates a new Vulkan planning backend facade.
|
|
||||||
#[must_use]
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
state: VulkanPlanningBackendState::Configured,
|
|
||||||
report: VulkanPlanningBackendReport::default(),
|
|
||||||
swapchain_plan: default_stage0_swapchain_plan(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replaces active surface/profile request.
|
|
||||||
pub fn set_render_request(&mut self, request: RenderRequest) {
|
|
||||||
if self.report.request.current_request != request {
|
|
||||||
self.report.request.current_request = request;
|
|
||||||
self.report.request.request_updates =
|
|
||||||
self.report.request.request_updates.saturating_add(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns active render request policy.
|
|
||||||
#[must_use]
|
|
||||||
pub const fn render_request(&self) -> RenderRequest {
|
|
||||||
self.report.request.current_request
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replaces active swapchain plan used for frame submission planning.
|
|
||||||
pub fn set_swapchain_plan(&mut self, plan: VulkanSwapchainPlan) {
|
|
||||||
self.swapchain_plan = plan;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns active swapchain plan.
|
|
||||||
#[must_use]
|
|
||||||
pub const fn swapchain_plan(&self) -> &VulkanSwapchainPlan {
|
|
||||||
&self.swapchain_plan
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns adapter state.
|
|
||||||
#[must_use]
|
|
||||||
pub const fn state(&self) -> VulkanPlanningBackendState {
|
|
||||||
self.state
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns backend report.
|
|
||||||
#[must_use]
|
|
||||||
pub fn report(&self) -> &VulkanPlanningBackendReport {
|
|
||||||
&self.report
|
|
||||||
}
|
|
||||||
|
|
||||||
fn simulate_present(&mut self) {
|
|
||||||
self.report.execution.simulated_presents =
|
|
||||||
self.report.execution.simulated_presents.saturating_add(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderBackend for VulkanPlanningBackend {
|
|
||||||
fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError> {
|
|
||||||
if !matches!(
|
|
||||||
self.state,
|
|
||||||
VulkanPlanningBackendState::Configured | VulkanPlanningBackendState::Degraded
|
|
||||||
) {
|
|
||||||
return Err(RenderError::InvalidRange);
|
|
||||||
}
|
|
||||||
let capture = canonical_capture(commands)?;
|
|
||||||
let frame_plan = plan_vulkan_frame_submission(&self.swapchain_plan, commands)?;
|
|
||||||
self.report.execution.planned_frames =
|
|
||||||
self.report.execution.planned_frames.saturating_add(1);
|
|
||||||
self.report.execution.submission_plans =
|
|
||||||
self.report.execution.submission_plans.saturating_add(1);
|
|
||||||
self.report.execution.last_capture_size = capture.len();
|
|
||||||
self.report.last_frame_submission = Some(frame_plan);
|
|
||||||
self.simulate_present();
|
|
||||||
Ok(FrameOutput)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_stage0_swapchain_plan() -> VulkanSwapchainPlan {
|
|
||||||
VulkanSwapchainPlan {
|
|
||||||
schema: 1,
|
|
||||||
extent: (1, 1),
|
|
||||||
format: VulkanSurfaceFormat {
|
|
||||||
format: vk::Format::B8G8R8A8_SRGB.as_raw(),
|
|
||||||
color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
|
|
||||||
},
|
|
||||||
present_mode: vk::PresentModeKHR::FIFO.as_raw(),
|
|
||||||
image_count: 2,
|
|
||||||
image_usage: vk::ImageUsageFlags::COLOR_ATTACHMENT.as_raw(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
use ash::vk;
|
use ash::vk;
|
||||||
use fparkan_platform::{DepthStencilSupport, RenderRequest};
|
use fparkan_platform::{DepthStencilSupport, RenderRequest};
|
||||||
use fparkan_render::{validate_command_list, RenderCommand, RenderCommandList, RenderError};
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
const MIN_VULKAN_API_VERSION: u32 = vk::API_VERSION_1_1;
|
const MIN_VULKAN_API_VERSION: u32 = vk::API_VERSION_1_1;
|
||||||
pub(crate) const KHR_SWAPCHAIN_EXTENSION: &str = "VK_KHR_swapchain";
|
pub(crate) const KHR_SWAPCHAIN_EXTENSION: &str = "VK_KHR_swapchain";
|
||||||
@@ -147,25 +145,6 @@ pub struct VulkanSwapchainRecreationReport {
|
|||||||
pub next_extent: (u32, u32),
|
pub next_extent: (u32, u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deterministic frame submission plan for command buffers and sync objects.
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
|
||||||
pub struct VulkanFrameSubmissionPlan {
|
|
||||||
/// Report schema version.
|
|
||||||
pub schema: u32,
|
|
||||||
/// Frames allowed in flight.
|
|
||||||
pub frames_in_flight: u32,
|
|
||||||
/// Swapchain-backed primary command buffers.
|
|
||||||
pub command_buffers: u32,
|
|
||||||
/// Binary semaphores allocated per frame.
|
|
||||||
pub semaphores_per_frame: u32,
|
|
||||||
/// Fences allocated per frame.
|
|
||||||
pub fences_per_frame: u32,
|
|
||||||
/// Draw commands encoded into the frame.
|
|
||||||
pub draw_count: u32,
|
|
||||||
/// Total indexed vertices submitted by draw commands.
|
|
||||||
pub indexed_vertex_count: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Synthetic physical-device capabilities used by negative tests and reports.
|
/// Synthetic physical-device capabilities used by negative tests and reports.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct VulkanPhysicalDeviceRecord {
|
pub struct VulkanPhysicalDeviceRecord {
|
||||||
@@ -204,7 +183,7 @@ impl VulkanPhysicalDeviceRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Informational device limits relevant to future Stage 0 capability growth.
|
/// Informational device limits relevant to future Stage 0 capability growth.
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub struct VulkanDeviceLimits {
|
pub struct VulkanDeviceLimits {
|
||||||
/// Maximum 2D image dimension supported by the device.
|
/// Maximum 2D image dimension supported by the device.
|
||||||
pub max_image_dimension_2d: u32,
|
pub max_image_dimension_2d: u32,
|
||||||
@@ -217,7 +196,7 @@ pub struct VulkanDeviceLimits {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Informational capabilities preserved in deterministic Stage 0 reports.
|
/// Informational capabilities preserved in deterministic Stage 0 reports.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct VulkanInformationalCapabilities {
|
pub struct VulkanInformationalCapabilities {
|
||||||
/// Color formats that support sampled-image usage.
|
/// Color formats that support sampled-image usage.
|
||||||
pub sampled_color_formats: Vec<i32>,
|
pub sampled_color_formats: Vec<i32>,
|
||||||
@@ -253,7 +232,7 @@ pub struct VulkanCapabilityReport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Deterministic rejection reason for an unsuitable physical device.
|
/// Deterministic rejection reason for an unsuitable physical device.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct VulkanRejectedDeviceReport {
|
pub struct VulkanRejectedDeviceReport {
|
||||||
/// Human-readable device name.
|
/// Human-readable device name.
|
||||||
pub device_name: String,
|
pub device_name: String,
|
||||||
@@ -446,136 +425,6 @@ pub const fn swapchain_recreation_report(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a deterministic frame submission plan for a validated command list.
|
|
||||||
///
|
|
||||||
/// Stage 0 keeps this as a pure planning boundary so command-pool, command-buffer
|
|
||||||
/// and synchronization policy can be tested without requiring a native surface.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns [`RenderError`] when the command list has invalid frame framing,
|
|
||||||
/// ordering, draw ranges, mesh bounds, or non-finite transforms.
|
|
||||||
pub fn plan_vulkan_frame_submission(
|
|
||||||
swapchain: &VulkanSwapchainPlan,
|
|
||||||
commands: &RenderCommandList,
|
|
||||||
) -> Result<VulkanFrameSubmissionPlan, RenderError> {
|
|
||||||
validate_command_list(commands)?;
|
|
||||||
let mut draw_count = 0_u32;
|
|
||||||
let mut indexed_vertex_count = 0_u32;
|
|
||||||
for command in &commands.commands {
|
|
||||||
if let RenderCommand::Draw(draw) = command {
|
|
||||||
draw_count = draw_count.saturating_add(1);
|
|
||||||
indexed_vertex_count = indexed_vertex_count.saturating_add(draw.range.count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(VulkanFrameSubmissionPlan {
|
|
||||||
schema: 1,
|
|
||||||
frames_in_flight: swapchain.image_count.clamp(1, 2),
|
|
||||||
command_buffers: swapchain.image_count,
|
|
||||||
semaphores_per_frame: 2,
|
|
||||||
fences_per_frame: 1,
|
|
||||||
draw_count,
|
|
||||||
indexed_vertex_count,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders a deterministic JSON capability report.
|
|
||||||
#[must_use]
|
|
||||||
pub fn render_capability_report_json(report: &VulkanCapabilityReport) -> String {
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct CapabilityReportJson<'a> {
|
|
||||||
schema: u32,
|
|
||||||
vulkan_api: String,
|
|
||||||
device_name: &'a str,
|
|
||||||
score: i32,
|
|
||||||
graphics_queue_family: u32,
|
|
||||||
present_queue_family: u32,
|
|
||||||
portability_subset: bool,
|
|
||||||
enabled_extensions: &'a [String],
|
|
||||||
informational_capabilities: &'a VulkanInformationalCapabilities,
|
|
||||||
rejected_devices: &'a [VulkanRejectedDeviceReport],
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize_json_or_fallback(
|
|
||||||
&CapabilityReportJson {
|
|
||||||
schema: report.schema,
|
|
||||||
vulkan_api: format_api_version(report.vulkan_api_version),
|
|
||||||
device_name: &report.device_name,
|
|
||||||
score: report.score,
|
|
||||||
graphics_queue_family: report.graphics_queue_family,
|
|
||||||
present_queue_family: report.present_queue_family,
|
|
||||||
portability_subset: report.portability_subset,
|
|
||||||
enabled_extensions: &report.enabled_extensions,
|
|
||||||
informational_capabilities: &report.informational_capabilities,
|
|
||||||
rejected_devices: &report.rejected_devices,
|
|
||||||
},
|
|
||||||
"{\"schema\":0,\"vulkan_api\":\"0.0.0\",\"device_name\":\"unknown\",\"score\":0,\"graphics_queue_family\":0,\"present_queue_family\":0,\"portability_subset\":false,\"enabled_extensions\":[],\"informational_capabilities\":{\"sampled_color_formats\":[],\"sampled_depth_formats\":[],\"limits\":{\"max_image_dimension_2d\":0,\"max_sampler_allocation_count\":0,\"max_per_stage_descriptor_samplers\":0,\"max_bound_descriptor_sets\":0}},\"rejected_devices\":[]}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders a deterministic JSON swapchain plan.
|
|
||||||
#[must_use]
|
|
||||||
pub fn render_swapchain_plan_json(plan: &VulkanSwapchainPlan) -> String {
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct SwapchainPlanJson {
|
|
||||||
schema: u32,
|
|
||||||
extent: [u32; 2],
|
|
||||||
format: i32,
|
|
||||||
color_space: i32,
|
|
||||||
present_mode: i32,
|
|
||||||
image_count: u32,
|
|
||||||
image_usage: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize_json_or_fallback(
|
|
||||||
&SwapchainPlanJson {
|
|
||||||
schema: plan.schema,
|
|
||||||
extent: [plan.extent.0, plan.extent.1],
|
|
||||||
format: plan.format.format,
|
|
||||||
color_space: plan.format.color_space,
|
|
||||||
present_mode: plan.present_mode,
|
|
||||||
image_count: plan.image_count,
|
|
||||||
image_usage: plan.image_usage,
|
|
||||||
},
|
|
||||||
"{\"schema\":0,\"extent\":[0,0],\"format\":0,\"color_space\":0,\"present_mode\":0,\"image_count\":0,\"image_usage\":0}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders a deterministic JSON swapchain recreation report.
|
|
||||||
#[must_use]
|
|
||||||
pub fn render_swapchain_recreation_report_json(report: &VulkanSwapchainRecreationReport) -> String {
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct SwapchainRecreationReportJson<'a> {
|
|
||||||
schema: u32,
|
|
||||||
reason: &'a str,
|
|
||||||
previous_extent: [u32; 2],
|
|
||||||
next_extent: [u32; 2],
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize_json_or_fallback(
|
|
||||||
&SwapchainRecreationReportJson {
|
|
||||||
schema: report.schema,
|
|
||||||
reason: match report.reason {
|
|
||||||
VulkanSwapchainRecreationReason::Resize => "resize",
|
|
||||||
VulkanSwapchainRecreationReason::OutOfDate => "out_of_date",
|
|
||||||
VulkanSwapchainRecreationReason::Suboptimal => "suboptimal",
|
|
||||||
},
|
|
||||||
previous_extent: [report.previous_extent.0, report.previous_extent.1],
|
|
||||||
next_extent: [report.next_extent.0, report.next_extent.1],
|
|
||||||
},
|
|
||||||
"{\"schema\":0,\"reason\":\"unknown\",\"previous_extent\":[0,0],\"next_extent\":[0,0]}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders a deterministic JSON frame submission plan.
|
|
||||||
#[must_use]
|
|
||||||
pub fn render_frame_submission_plan_json(plan: &VulkanFrameSubmissionPlan) -> String {
|
|
||||||
serialize_json_or_fallback(
|
|
||||||
plan,
|
|
||||||
"{\"schema\":0,\"frames_in_flight\":0,\"command_buffers\":0,\"semaphores_per_frame\":0,\"fences_per_frame\":0,\"draw_count\":0,\"indexed_vertex_count\":0}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn select_composite_alpha(
|
pub(crate) fn select_composite_alpha(
|
||||||
supported: vk::CompositeAlphaFlagsKHR,
|
supported: vk::CompositeAlphaFlagsKHR,
|
||||||
) -> vk::CompositeAlphaFlagsKHR {
|
) -> vk::CompositeAlphaFlagsKHR {
|
||||||
@@ -590,13 +439,6 @@ pub(crate) fn select_composite_alpha(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn serialize_json_or_fallback<T: Serialize>(value: &T, fallback: &str) -> String {
|
|
||||||
match serde_json::to_string(value) {
|
|
||||||
Ok(json) => json,
|
|
||||||
Err(_) => fallback.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn format_api_version(version: u32) -> String {
|
pub(crate) fn format_api_version(version: u32) -> String {
|
||||||
format!(
|
format!(
|
||||||
"{}.{}.{}",
|
"{}.{}.{}",
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
use crate::ffi::{
|
||||||
|
SPIRV_MAGIC, SPIRV_VERSION_1_0, TRIANGLE_FRAGMENT_SHADER_WORDS, TRIANGLE_VERTEX_SHADER_WORDS,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Error found while checking a checked-in SPIR-V module.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum VulkanShaderError {
|
||||||
|
/// SPIR-V module is too short to contain a header.
|
||||||
|
TooShort {
|
||||||
|
/// Shader name.
|
||||||
|
name: &'static str,
|
||||||
|
},
|
||||||
|
/// SPIR-V module has an invalid magic word.
|
||||||
|
InvalidMagic {
|
||||||
|
/// Shader name.
|
||||||
|
name: &'static str,
|
||||||
|
/// Found magic word.
|
||||||
|
found: u32,
|
||||||
|
},
|
||||||
|
/// SPIR-V module version is below 1.0.
|
||||||
|
UnsupportedVersion {
|
||||||
|
/// Shader name.
|
||||||
|
name: &'static str,
|
||||||
|
/// Found version word.
|
||||||
|
found: u32,
|
||||||
|
},
|
||||||
|
/// SPIR-V module declares an invalid bound.
|
||||||
|
InvalidBound {
|
||||||
|
/// Shader name.
|
||||||
|
name: &'static str,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for VulkanShaderError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::TooShort { name } => write!(f, "shader {name} SPIR-V module is too short"),
|
||||||
|
Self::InvalidMagic { name, found } => {
|
||||||
|
write!(f, "shader {name} has invalid SPIR-V magic 0x{found:08x}")
|
||||||
|
}
|
||||||
|
Self::UnsupportedVersion { name, found } => write!(
|
||||||
|
f,
|
||||||
|
"shader {name} has unsupported SPIR-V version 0x{found:08x}"
|
||||||
|
),
|
||||||
|
Self::InvalidBound { name } => write!(f, "shader {name} has invalid SPIR-V bound"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for VulkanShaderError {}
|
||||||
|
|
||||||
|
/// Checks the checked-in triangle shader containers before creating a pipeline.
|
||||||
|
pub(crate) fn validate_triangle_shaders() -> Result<(), VulkanShaderError> {
|
||||||
|
validate_spirv_container("triangle.vert", TRIANGLE_VERTEX_SHADER_WORDS)?;
|
||||||
|
validate_spirv_container("triangle.frag", TRIANGLE_FRAGMENT_SHADER_WORDS)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn validate_spirv_container(
|
||||||
|
name: &'static str,
|
||||||
|
words: &[u32],
|
||||||
|
) -> Result<(), VulkanShaderError> {
|
||||||
|
if words.len() < 5 {
|
||||||
|
return Err(VulkanShaderError::TooShort { name });
|
||||||
|
}
|
||||||
|
if words[0] != SPIRV_MAGIC {
|
||||||
|
return Err(VulkanShaderError::InvalidMagic {
|
||||||
|
name,
|
||||||
|
found: words[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if words[1] < SPIRV_VERSION_1_0 {
|
||||||
|
return Err(VulkanShaderError::UnsupportedVersion {
|
||||||
|
name,
|
||||||
|
found: words[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if words[3] == 0 {
|
||||||
|
return Err(VulkanShaderError::InvalidBound { name });
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,399 +0,0 @@
|
|||||||
use fparkan_binary::{sha256, sha256_hex};
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
pub(crate) use crate::ffi::{
|
|
||||||
SPIRV_MAGIC, SPIRV_VERSION_1_0, TRIANGLE_FRAGMENT_SHADER_WORDS, TRIANGLE_VERTEX_SHADER_WORDS,
|
|
||||||
};
|
|
||||||
use crate::policy::serialize_json_or_fallback;
|
|
||||||
|
|
||||||
pub(crate) const SHADER_MANIFEST_SCHEMA: u32 = 2;
|
|
||||||
pub(crate) const SHADER_TARGET_ENV: &str = "vulkan1.1";
|
|
||||||
pub(crate) const SHADER_COMPILER_NAME: &str = "glslangValidator";
|
|
||||||
pub(crate) const SHADER_COMPILER_VERSION: &str = "11:16.3.0";
|
|
||||||
pub(crate) const SHADER_COMPILER_BINARY_SHA256: &str =
|
|
||||||
"9bcd69d830b350aaa6e2254915ff74e46070e217b67f38daad27c1fc1f22910f";
|
|
||||||
pub(crate) const SPIRV_VALIDATOR_NAME: &str = "spirv-val";
|
|
||||||
pub(crate) const SPIRV_VALIDATOR_VERSION: &str =
|
|
||||||
"SPIRV-Tools v2026.2 unknown hash, 2026-04-29T17:02:58+00:00";
|
|
||||||
pub(crate) const SPIRV_VALIDATOR_BINARY_SHA256: &str =
|
|
||||||
"f6d5b96ff19f073f3af0c0bcfa0c18702d288d3ec598efc242d01cd104d8354f";
|
|
||||||
pub(crate) const TRIANGLE_VERTEX_SOURCE_PATH: &str =
|
|
||||||
"adapters/fparkan-render-vulkan/shaders/triangle.vert";
|
|
||||||
pub(crate) const TRIANGLE_VERTEX_SOURCE_SHA256: &str =
|
|
||||||
"fe3721202477220d2d9677b6572d7f3baecb374e94f04eea9d7286ed5e98b6c9";
|
|
||||||
pub(crate) const TRIANGLE_VERTEX_SPIRV_PATH: &str =
|
|
||||||
"adapters/fparkan-render-vulkan/shaders/triangle.vert.spv";
|
|
||||||
pub(crate) const TRIANGLE_VERTEX_COMPILE_COMMAND: &str =
|
|
||||||
"glslangValidator -V --target-env vulkan1.1 -S vert -e main adapters/fparkan-render-vulkan/shaders/triangle.vert -o adapters/fparkan-render-vulkan/shaders/triangle.vert.spv";
|
|
||||||
pub(crate) const TRIANGLE_VERTEX_VALIDATE_COMMAND: &str =
|
|
||||||
"spirv-val --target-env vulkan1.1 adapters/fparkan-render-vulkan/shaders/triangle.vert.spv";
|
|
||||||
const TRIANGLE_FRAGMENT_SOURCE_PATH: &str = "adapters/fparkan-render-vulkan/shaders/triangle.frag";
|
|
||||||
const TRIANGLE_FRAGMENT_SOURCE_SHA256: &str =
|
|
||||||
"fb3ed435af48e4fb4a817f160b467f7a561c7c547c87b6f0c1e89f5f58af7329";
|
|
||||||
const TRIANGLE_FRAGMENT_SPIRV_PATH: &str =
|
|
||||||
"adapters/fparkan-render-vulkan/shaders/triangle.frag.spv";
|
|
||||||
const TRIANGLE_FRAGMENT_COMPILE_COMMAND: &str =
|
|
||||||
"glslangValidator -V --target-env vulkan1.1 -S frag -e main adapters/fparkan-render-vulkan/shaders/triangle.frag -o adapters/fparkan-render-vulkan/shaders/triangle.frag.spv";
|
|
||||||
const TRIANGLE_FRAGMENT_VALIDATE_COMMAND: &str =
|
|
||||||
"spirv-val --target-env vulkan1.1 adapters/fparkan-render-vulkan/shaders/triangle.frag.spv";
|
|
||||||
|
|
||||||
fn shader_compiler_name() -> &'static str {
|
|
||||||
option_env!("FPARKAN_BUILD_SHADER_COMPILER_NAME").unwrap_or(SHADER_COMPILER_NAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shader_compiler_version() -> &'static str {
|
|
||||||
option_env!("FPARKAN_BUILD_SHADER_COMPILER_VERSION").unwrap_or(SHADER_COMPILER_VERSION)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shader_compiler_binary_sha256() -> &'static str {
|
|
||||||
option_env!("FPARKAN_BUILD_SHADER_COMPILER_SHA256").unwrap_or(SHADER_COMPILER_BINARY_SHA256)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spirv_validator_name() -> &'static str {
|
|
||||||
option_env!("FPARKAN_BUILD_SPIRV_VALIDATOR_NAME").unwrap_or(SPIRV_VALIDATOR_NAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spirv_validator_version() -> &'static str {
|
|
||||||
option_env!("FPARKAN_BUILD_SPIRV_VALIDATOR_VERSION").unwrap_or(SPIRV_VALIDATOR_VERSION)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spirv_validator_binary_sha256() -> &'static str {
|
|
||||||
option_env!("FPARKAN_BUILD_SPIRV_VALIDATOR_SHA256").unwrap_or(SPIRV_VALIDATOR_BINARY_SHA256)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shader tool metadata pinned in the Stage 0 manifest.
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
|
||||||
pub struct VulkanShaderToolManifest {
|
|
||||||
/// Tool executable name.
|
|
||||||
pub name: &'static str,
|
|
||||||
/// Tool version string.
|
|
||||||
pub version: &'static str,
|
|
||||||
/// Tool binary SHA-256.
|
|
||||||
pub binary_sha256: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Vulkan shader stage.
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum VulkanShaderStage {
|
|
||||||
/// Vertex stage.
|
|
||||||
Vertex,
|
|
||||||
/// Fragment stage.
|
|
||||||
Fragment,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Offline SPIR-V shader manifest entry.
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct VulkanShaderModuleManifest {
|
|
||||||
/// Logical shader name.
|
|
||||||
pub name: &'static str,
|
|
||||||
/// Shader stage.
|
|
||||||
pub stage: VulkanShaderStage,
|
|
||||||
/// SPIR-V entry point.
|
|
||||||
pub entry_point: &'static str,
|
|
||||||
/// Descriptor set count.
|
|
||||||
pub descriptor_sets: u32,
|
|
||||||
/// Push constant byte count.
|
|
||||||
pub push_constant_bytes: u32,
|
|
||||||
/// Checked-in GLSL source path.
|
|
||||||
pub source_path: &'static str,
|
|
||||||
/// Checked-in GLSL source SHA-256.
|
|
||||||
pub source_sha256: &'static str,
|
|
||||||
/// Checked-in SPIR-V module path.
|
|
||||||
pub spirv_path: &'static str,
|
|
||||||
/// Exact offline compile command used for the checked-in SPIR-V artifact.
|
|
||||||
pub compile_command: &'static str,
|
|
||||||
/// Exact offline validation command used for the checked-in SPIR-V artifact.
|
|
||||||
pub validate_command: &'static str,
|
|
||||||
/// SPIR-V words.
|
|
||||||
pub words: &'static [u32],
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shader manifest validation report.
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct VulkanShaderManifestReport {
|
|
||||||
/// Report schema version.
|
|
||||||
pub schema: u32,
|
|
||||||
/// Explicit Vulkan target environment for the checked-in SPIR-V.
|
|
||||||
pub target_env: &'static str,
|
|
||||||
/// Pinned compiler metadata.
|
|
||||||
pub compiler: VulkanShaderToolManifest,
|
|
||||||
/// Pinned validator metadata.
|
|
||||||
pub validator: VulkanShaderToolManifest,
|
|
||||||
/// Shader module reports.
|
|
||||||
pub modules: Vec<VulkanShaderModuleReport>,
|
|
||||||
/// Hash of the normalized shader manifest.
|
|
||||||
pub manifest_hash: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shader module validation report.
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
|
||||||
pub struct VulkanShaderModuleReport {
|
|
||||||
/// Logical shader name.
|
|
||||||
pub name: &'static str,
|
|
||||||
/// Shader stage.
|
|
||||||
pub stage: VulkanShaderStage,
|
|
||||||
/// SPIR-V entry point.
|
|
||||||
pub entry_point: &'static str,
|
|
||||||
/// Checked-in GLSL source path.
|
|
||||||
pub source_path: &'static str,
|
|
||||||
/// Checked-in GLSL source SHA-256.
|
|
||||||
pub source_sha256: &'static str,
|
|
||||||
/// Checked-in SPIR-V module path.
|
|
||||||
pub spirv_path: &'static str,
|
|
||||||
/// SPIR-V word count.
|
|
||||||
pub word_count: usize,
|
|
||||||
/// SPIR-V byte hash.
|
|
||||||
pub sha256: String,
|
|
||||||
/// Descriptor set count.
|
|
||||||
pub descriptor_sets: u32,
|
|
||||||
/// Push constant byte count.
|
|
||||||
pub push_constant_bytes: u32,
|
|
||||||
/// Exact offline compile command used for the checked-in SPIR-V artifact.
|
|
||||||
pub compile_command: &'static str,
|
|
||||||
/// Exact offline validation command used for the checked-in SPIR-V artifact.
|
|
||||||
pub validate_command: &'static str,
|
|
||||||
/// Stable hash of the reflected interface contract for this module.
|
|
||||||
pub interface_hash: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shader manifest validation error.
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub enum VulkanShaderManifestError {
|
|
||||||
/// SPIR-V module is too short to contain a header.
|
|
||||||
TooShort {
|
|
||||||
/// Shader name.
|
|
||||||
name: &'static str,
|
|
||||||
},
|
|
||||||
/// SPIR-V module has an invalid magic word.
|
|
||||||
InvalidMagic {
|
|
||||||
/// Shader name.
|
|
||||||
name: &'static str,
|
|
||||||
/// Found magic word.
|
|
||||||
found: u32,
|
|
||||||
},
|
|
||||||
/// SPIR-V module version is below 1.0.
|
|
||||||
UnsupportedVersion {
|
|
||||||
/// Shader name.
|
|
||||||
name: &'static str,
|
|
||||||
/// Found version word.
|
|
||||||
found: u32,
|
|
||||||
},
|
|
||||||
/// SPIR-V module declares an invalid bound.
|
|
||||||
InvalidBound {
|
|
||||||
/// Shader name.
|
|
||||||
name: &'static str,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for VulkanShaderManifestError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::TooShort { name } => write!(f, "shader {name} SPIR-V module is too short"),
|
|
||||||
Self::InvalidMagic { name, found } => {
|
|
||||||
write!(f, "shader {name} has invalid SPIR-V magic 0x{found:08x}")
|
|
||||||
}
|
|
||||||
Self::UnsupportedVersion { name, found } => write!(
|
|
||||||
f,
|
|
||||||
"shader {name} has unsupported SPIR-V version 0x{found:08x}"
|
|
||||||
),
|
|
||||||
Self::InvalidBound { name } => write!(f, "shader {name} has invalid SPIR-V bound"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for VulkanShaderManifestError {}
|
|
||||||
|
|
||||||
/// Returns the built-in Stage 0 indexed-triangle shader manifest.
|
|
||||||
#[must_use]
|
|
||||||
pub fn triangle_shader_manifest() -> Vec<VulkanShaderModuleManifest> {
|
|
||||||
vec![
|
|
||||||
VulkanShaderModuleManifest {
|
|
||||||
name: "triangle.vert",
|
|
||||||
stage: VulkanShaderStage::Vertex,
|
|
||||||
entry_point: "main",
|
|
||||||
descriptor_sets: 0,
|
|
||||||
push_constant_bytes: 64,
|
|
||||||
source_path: TRIANGLE_VERTEX_SOURCE_PATH,
|
|
||||||
source_sha256: TRIANGLE_VERTEX_SOURCE_SHA256,
|
|
||||||
spirv_path: TRIANGLE_VERTEX_SPIRV_PATH,
|
|
||||||
compile_command: TRIANGLE_VERTEX_COMPILE_COMMAND,
|
|
||||||
validate_command: TRIANGLE_VERTEX_VALIDATE_COMMAND,
|
|
||||||
words: TRIANGLE_VERTEX_SHADER_WORDS,
|
|
||||||
},
|
|
||||||
VulkanShaderModuleManifest {
|
|
||||||
name: "triangle.frag",
|
|
||||||
stage: VulkanShaderStage::Fragment,
|
|
||||||
entry_point: "main",
|
|
||||||
descriptor_sets: 1,
|
|
||||||
push_constant_bytes: 68,
|
|
||||||
source_path: TRIANGLE_FRAGMENT_SOURCE_PATH,
|
|
||||||
source_sha256: TRIANGLE_FRAGMENT_SOURCE_SHA256,
|
|
||||||
spirv_path: TRIANGLE_FRAGMENT_SPIRV_PATH,
|
|
||||||
compile_command: TRIANGLE_FRAGMENT_COMPILE_COMMAND,
|
|
||||||
validate_command: TRIANGLE_FRAGMENT_VALIDATE_COMMAND,
|
|
||||||
words: TRIANGLE_FRAGMENT_SHADER_WORDS,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validates shader SPIR-V containers and renders a deterministic report.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns [`VulkanShaderManifestError`] when a module fails Stage 0 SPIR-V
|
|
||||||
/// container validation.
|
|
||||||
pub fn validate_shader_manifest(
|
|
||||||
modules: &[VulkanShaderModuleManifest],
|
|
||||||
) -> Result<VulkanShaderManifestReport, VulkanShaderManifestError> {
|
|
||||||
let mut reports = Vec::with_capacity(modules.len());
|
|
||||||
for module in modules {
|
|
||||||
validate_spirv_container(module)?;
|
|
||||||
let bytes = spirv_words_to_bytes(module.words);
|
|
||||||
reports.push(VulkanShaderModuleReport {
|
|
||||||
name: module.name,
|
|
||||||
stage: module.stage,
|
|
||||||
entry_point: module.entry_point,
|
|
||||||
source_path: module.source_path,
|
|
||||||
source_sha256: module.source_sha256,
|
|
||||||
spirv_path: module.spirv_path,
|
|
||||||
word_count: module.words.len(),
|
|
||||||
sha256: sha256_hex(&sha256(&bytes)),
|
|
||||||
descriptor_sets: module.descriptor_sets,
|
|
||||||
push_constant_bytes: module.push_constant_bytes,
|
|
||||||
compile_command: module.compile_command,
|
|
||||||
validate_command: module.validate_command,
|
|
||||||
interface_hash: shader_interface_hash(module),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let normalized = render_shader_manifest_without_hash_json(&reports);
|
|
||||||
Ok(VulkanShaderManifestReport {
|
|
||||||
schema: SHADER_MANIFEST_SCHEMA,
|
|
||||||
target_env: SHADER_TARGET_ENV,
|
|
||||||
compiler: VulkanShaderToolManifest {
|
|
||||||
name: shader_compiler_name(),
|
|
||||||
version: shader_compiler_version(),
|
|
||||||
binary_sha256: shader_compiler_binary_sha256(),
|
|
||||||
},
|
|
||||||
validator: VulkanShaderToolManifest {
|
|
||||||
name: spirv_validator_name(),
|
|
||||||
version: spirv_validator_version(),
|
|
||||||
binary_sha256: spirv_validator_binary_sha256(),
|
|
||||||
},
|
|
||||||
modules: reports,
|
|
||||||
manifest_hash: sha256_hex(&sha256(normalized.as_bytes())),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders a deterministic JSON shader manifest report.
|
|
||||||
#[must_use]
|
|
||||||
pub fn render_shader_manifest_report_json(report: &VulkanShaderManifestReport) -> String {
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct ShaderManifestReportJson<'a> {
|
|
||||||
schema: u32,
|
|
||||||
target_env: &'a str,
|
|
||||||
compiler: &'a VulkanShaderToolManifest,
|
|
||||||
validator: &'a VulkanShaderToolManifest,
|
|
||||||
modules: &'a [VulkanShaderModuleReport],
|
|
||||||
manifest_hash: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize_json_or_fallback(
|
|
||||||
&ShaderManifestReportJson {
|
|
||||||
schema: report.schema,
|
|
||||||
target_env: report.target_env,
|
|
||||||
compiler: &report.compiler,
|
|
||||||
validator: &report.validator,
|
|
||||||
modules: &report.modules,
|
|
||||||
manifest_hash: &report.manifest_hash,
|
|
||||||
},
|
|
||||||
"{\"schema\":0,\"target_env\":\"unknown\",\"compiler\":{\"name\":\"unknown\",\"version\":\"unknown\",\"binary_sha256\":\"unknown\"},\"validator\":{\"name\":\"unknown\",\"version\":\"unknown\",\"binary_sha256\":\"unknown\"},\"modules\":[],\"manifest_hash\":\"unknown\"}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shader_interface_hash(module: &VulkanShaderModuleManifest) -> String {
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct ShaderInterfaceHashJson<'a> {
|
|
||||||
stage: VulkanShaderStage,
|
|
||||||
entry_point: &'a str,
|
|
||||||
descriptor_sets: u32,
|
|
||||||
push_constant_bytes: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
let normalized = serialize_json_or_fallback(
|
|
||||||
&ShaderInterfaceHashJson {
|
|
||||||
stage: module.stage,
|
|
||||||
entry_point: module.entry_point,
|
|
||||||
descriptor_sets: module.descriptor_sets,
|
|
||||||
push_constant_bytes: module.push_constant_bytes,
|
|
||||||
},
|
|
||||||
"{\"stage\":\"vertex\",\"entry_point\":\"main\",\"descriptor_sets\":0,\"push_constant_bytes\":0}",
|
|
||||||
);
|
|
||||||
sha256_hex(&sha256(normalized.as_bytes()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_spirv_container(
|
|
||||||
module: &VulkanShaderModuleManifest,
|
|
||||||
) -> Result<(), VulkanShaderManifestError> {
|
|
||||||
if module.words.len() < 5 {
|
|
||||||
return Err(VulkanShaderManifestError::TooShort { name: module.name });
|
|
||||||
}
|
|
||||||
if module.words[0] != SPIRV_MAGIC {
|
|
||||||
return Err(VulkanShaderManifestError::InvalidMagic {
|
|
||||||
name: module.name,
|
|
||||||
found: module.words[0],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if module.words[1] < SPIRV_VERSION_1_0 {
|
|
||||||
return Err(VulkanShaderManifestError::UnsupportedVersion {
|
|
||||||
name: module.name,
|
|
||||||
found: module.words[1],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if module.words[3] == 0 {
|
|
||||||
return Err(VulkanShaderManifestError::InvalidBound { name: module.name });
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spirv_words_to_bytes(words: &[u32]) -> Vec<u8> {
|
|
||||||
let mut out = Vec::with_capacity(words.len() * 4);
|
|
||||||
for word in words {
|
|
||||||
out.extend_from_slice(&word.to_le_bytes());
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_shader_manifest_without_hash_json(modules: &[VulkanShaderModuleReport]) -> String {
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct ShaderManifestWithoutHashJson<'a> {
|
|
||||||
schema: u32,
|
|
||||||
target_env: &'a str,
|
|
||||||
compiler: VulkanShaderToolManifest,
|
|
||||||
validator: VulkanShaderToolManifest,
|
|
||||||
modules: &'a [VulkanShaderModuleReport],
|
|
||||||
}
|
|
||||||
|
|
||||||
let json = serialize_json_or_fallback(
|
|
||||||
&ShaderManifestWithoutHashJson {
|
|
||||||
schema: SHADER_MANIFEST_SCHEMA,
|
|
||||||
target_env: SHADER_TARGET_ENV,
|
|
||||||
compiler: VulkanShaderToolManifest {
|
|
||||||
name: SHADER_COMPILER_NAME,
|
|
||||||
version: SHADER_COMPILER_VERSION,
|
|
||||||
binary_sha256: SHADER_COMPILER_BINARY_SHA256,
|
|
||||||
},
|
|
||||||
validator: VulkanShaderToolManifest {
|
|
||||||
name: SPIRV_VALIDATOR_NAME,
|
|
||||||
version: SPIRV_VALIDATOR_VERSION,
|
|
||||||
binary_sha256: SPIRV_VALIDATOR_BINARY_SHA256,
|
|
||||||
},
|
|
||||||
modules,
|
|
||||||
},
|
|
||||||
"{\"schema\":0,\"target_env\":\"unknown\",\"compiler\":{\"name\":\"unknown\",\"version\":\"unknown\",\"binary_sha256\":\"unknown\"},\"validator\":{\"name\":\"unknown\",\"version\":\"unknown\",\"binary_sha256\":\"unknown\"},\"modules\":[]}",
|
|
||||||
);
|
|
||||||
match json.strip_suffix('}') {
|
|
||||||
Some(stripped) => stripped.to_string(),
|
|
||||||
None => json,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# ADR-0001: Modular Monolith
|
|
||||||
|
|
||||||
Status: accepted
|
|
||||||
|
|
||||||
FParkan is implemented as one Cargo workspace with local crates grouped by domain. Binaries and adapters compose domain crates; domain crates do not import platform, windowing, OpenGL, GUI, or application packages.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# ADR-0002: Behavior Compatibility, Not ABI Compatibility
|
|
||||||
|
|
||||||
Status: accepted
|
|
||||||
|
|
||||||
The project targets clean-room behavior compatibility for formats, resource lookup, loading order, deterministic runtime behavior, and presentation command semantics. It does not reproduce original DLL boundaries, exports, calling conventions, object layouts, RVAs, or native singleton access patterns.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# ADR-0003: Raw And Interpreted Data
|
|
||||||
|
|
||||||
Status: accepted
|
|
||||||
|
|
||||||
Legacy data models keep raw bytes distinct from validated structure and interpreted domain views. Writers preserve raw data unless an explicit editing profile requests canonical rebuilding.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# ADR-0004: Synthetic And Licensed Tests
|
|
||||||
|
|
||||||
Status: accepted
|
|
||||||
|
|
||||||
Synthetic tests run everywhere and contain no proprietary data. Licensed corpus tests require an explicit local manifest and fail when requested without configuration. Reports contain metrics and fingerprints, not payload dumps or absolute game roots.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# ADR-0005: Deterministic Reference Runtime
|
|
||||||
|
|
||||||
Status: accepted
|
|
||||||
|
|
||||||
Stages 0-5 use a single-threaded deterministic reference profile. Stable ordering, explicit ticks, named random streams, and canonical captures are part of the contract. Wall-clock time, pointer addresses, hash iteration order, and GPU handles are not semantic inputs.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# ADR-0006: Error Policy
|
|
||||||
|
|
||||||
Status: accepted
|
|
||||||
|
|
||||||
Missing required resources, malformed bytes, unsupported documented branches, capability mismatches, and budget failures are structured errors. Runtime code must not silently skip mandatory objects or convert corrupted data into empty success values.
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# ADR-0007: Safe SDL/OpenGL Boundary
|
|
||||||
|
|
||||||
Status: provisional
|
|
||||||
|
|
||||||
Workspace-owned code forbids `unsafe`. SDL/OpenGL adapters must use maintained
|
|
||||||
external crates behind a safe project API; local Objective-C/CGL/SDL/OpenGL FFI
|
|
||||||
inside FParkan is not an acceptable implementation strategy.
|
|
||||||
|
|
||||||
The current adapter crates are safe boundary stubs. They compile the intended
|
|
||||||
ports and deterministic command contracts, but they do not create SDL windows,
|
|
||||||
GL contexts, GPU resources, shaders, draw calls, swapchains, or presents. They
|
|
||||||
must not be treated as backend readiness evidence.
|
|
||||||
|
|
||||||
To close the macOS backend requirement, choose and vendor/lock a maintained
|
|
||||||
safe facade stack, then implement:
|
|
||||||
|
|
||||||
- SDL event source, window creation, GL context lifecycle, drawable size and
|
|
||||||
present;
|
|
||||||
- GL shader compile/link, buffer/texture upload, render state, draw calls and
|
|
||||||
diagnostics;
|
|
||||||
- game/viewer composition roots using those adapters;
|
|
||||||
- hidden-window/offscreen macOS smoke tests and licensed local model/terrain
|
|
||||||
frame captures.
|
|
||||||
|
|
||||||
Until those are implemented, Desktop GL evidence may document external probes
|
|
||||||
only; it does not satisfy the permanent adapter requirement.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "fparkan-cli"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
repository.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
fparkan-assets = { path = "../../crates/fparkan-assets", version = "0.1.0" }
|
|
||||||
fparkan-corpus = { path = "../../crates/fparkan-corpus", version = "0.1.0" }
|
|
||||||
fparkan-prototype = { path = "../../crates/fparkan-prototype", version = "0.1.0" }
|
|
||||||
fparkan-inspection = { path = "../../crates/fparkan-inspection", version = "0.1.0" }
|
|
||||||
fparkan-path = { path = "../../crates/fparkan-path", version = "0.1.0" }
|
|
||||||
fparkan-resource = { path = "../../crates/fparkan-resource", version = "0.1.0" }
|
|
||||||
fparkan-runtime = { path = "../../crates/fparkan-runtime", version = "0.1.0" }
|
|
||||||
fparkan-script = { path = "../../crates/fparkan-script", version = "0.1.0" }
|
|
||||||
fparkan-vfs = { path = "../../crates/fparkan-vfs", version = "0.1.0" }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -6,20 +6,14 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fparkan-assets = { path = "../../crates/fparkan-assets", version = "0.1.0" }
|
fparkan-assets = { path = "../../crates/fparkan-assets" }
|
||||||
fparkan-inspection = { path = "../../crates/fparkan-inspection", version = "0.1.0" }
|
fparkan-path = { path = "../../crates/fparkan-path" }
|
||||||
fparkan-path = { path = "../../crates/fparkan-path", version = "0.1.0" }
|
fparkan-render = { path = "../../crates/fparkan-render" }
|
||||||
fparkan-platform = { path = "../../crates/fparkan-platform", version = "0.1.0" }
|
fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit" }
|
||||||
fparkan-render = { path = "../../crates/fparkan-render", version = "0.1.0" }
|
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" }
|
||||||
fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit", version = "0.1.0" }
|
fparkan-runtime = { path = "../../crates/fparkan-runtime" }
|
||||||
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan", version = "0.1.0" }
|
fparkan-terrain = { path = "../../crates/fparkan-terrain" }
|
||||||
fparkan-runtime = { path = "../../crates/fparkan-runtime", version = "0.1.0" }
|
fparkan-vfs = { path = "../../crates/fparkan-vfs" }
|
||||||
fparkan-terrain = { path = "../../crates/fparkan-terrain", version = "0.1.0" }
|
|
||||||
fparkan-vfs = { path = "../../crates/fparkan-vfs", version = "0.1.0" }
|
|
||||||
fparkan-world = { path = "../../crates/fparkan-world", version = "0.1.0" }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
winit = { version = "0.30", default-features = false, features = ["rwh_06"] }
|
winit = { version = "0.30", default-features = false, features = ["rwh_06"] }
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
+96
-947
File diff suppressed because it is too large
Load Diff
@@ -1,14 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "fparkan-headless"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
repository.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
fparkan-runtime = { path = "../../crates/fparkan-runtime", version = "0.1.0" }
|
|
||||||
fparkan-vfs = { path = "../../crates/fparkan-vfs", version = "0.1.0" }
|
|
||||||
fparkan-world = { path = "../../crates/fparkan-world", version = "0.1.0" }
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
#![forbid(unsafe_code)]
|
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
#![allow(clippy::print_stderr, clippy::print_stdout)]
|
|
||||||
//! `FParkan` headless runtime entrypoint.
|
|
||||||
|
|
||||||
use fparkan_runtime::{
|
|
||||||
advance_reference_movement, create, load_mission, step_headless, EngineConfig, EngineMode,
|
|
||||||
EngineServices, MissionRequest,
|
|
||||||
};
|
|
||||||
use fparkan_vfs::DirectoryVfs;
|
|
||||||
use fparkan_world::{InputSnapshot, OriginalObjectId};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
if let Err(err) = run() {
|
|
||||||
eprintln!("{err}");
|
|
||||||
std::process::exit(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run() -> Result<(), String> {
|
|
||||||
let raw_args: Vec<String> = std::env::args().skip(1).collect();
|
|
||||||
let args = Args::parse(&raw_args)?;
|
|
||||||
let services = if let Some(root) = &args.root {
|
|
||||||
EngineServices::new(Arc::new(DirectoryVfs::new(root)))
|
|
||||||
} else {
|
|
||||||
EngineServices::default()
|
|
||||||
};
|
|
||||||
let mut engine = create(
|
|
||||||
EngineConfig {
|
|
||||||
mode: EngineMode::Headless,
|
|
||||||
},
|
|
||||||
services,
|
|
||||||
)
|
|
||||||
.map_err(|err| format!("{err}"))?;
|
|
||||||
if let Some(mission) = args.mission {
|
|
||||||
let loaded = load_mission(&mut engine, MissionRequest { key: mission })
|
|
||||||
.map_err(|err| format!("{err}"))?;
|
|
||||||
println!(
|
|
||||||
"mission objects={} areals={} surfaces={} graph_roots={} components={} wear={} material_slots={} textures={} lightmaps={} scripts={} script_events={} script_varset_declarations={} script_init_states={} script_varset_states={} graph_failures={}",
|
|
||||||
loaded.object_count,
|
|
||||||
loaded.areal_count,
|
|
||||||
loaded.surface_count,
|
|
||||||
loaded.graph_root_count,
|
|
||||||
loaded.graph_unit_component_count,
|
|
||||||
loaded.graph_wear_resolved_count,
|
|
||||||
loaded.graph_material_resolved_count,
|
|
||||||
loaded.graph_texture_resolved_count,
|
|
||||||
loaded.graph_lightmap_resolved_count,
|
|
||||||
loaded.script_bundle_count,
|
|
||||||
loaded.script_event_count,
|
|
||||||
loaded.script_varset_declaration_count,
|
|
||||||
loaded.script_init_state_count,
|
|
||||||
loaded.script_varset_state_count,
|
|
||||||
loaded.graph_failure_count
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(movement) = args.reference_movement {
|
|
||||||
let reached = advance_reference_movement(
|
|
||||||
&mut engine,
|
|
||||||
OriginalObjectId(movement.original_id),
|
|
||||||
movement.target_xy,
|
|
||||||
movement.max_step,
|
|
||||||
)
|
|
||||||
.map_err(|err| format!("{err}"))?;
|
|
||||||
println!(
|
|
||||||
"reference_movement original_id={} target_xy=[{},{}] max_step={} reached={reached}",
|
|
||||||
movement.original_id, movement.target_xy[0], movement.target_xy[1], movement.max_step,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let mut last = None;
|
|
||||||
for _ in 0..args.ticks {
|
|
||||||
last = Some(step_headless(&mut engine, InputSnapshot).map_err(|err| format!("{err}"))?);
|
|
||||||
}
|
|
||||||
if let Some(frame) = last {
|
|
||||||
println!(
|
|
||||||
"tick={} hash={:02x?}",
|
|
||||||
frame.snapshot.tick.0, frame.snapshot.hash.0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct Args {
|
|
||||||
root: Option<PathBuf>,
|
|
||||||
mission: Option<String>,
|
|
||||||
ticks: u64,
|
|
||||||
reference_movement: Option<ReferenceMovement>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
||||||
struct ReferenceMovement {
|
|
||||||
original_id: u32,
|
|
||||||
target_xy: [f32; 2],
|
|
||||||
max_step: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Args {
|
|
||||||
fn parse(args: &[String]) -> Result<Self, String> {
|
|
||||||
let mut parsed = Self {
|
|
||||||
root: None,
|
|
||||||
mission: None,
|
|
||||||
ticks: 1,
|
|
||||||
reference_movement: None,
|
|
||||||
};
|
|
||||||
let mut iter = args.iter();
|
|
||||||
while let Some(arg) = iter.next() {
|
|
||||||
match arg.as_str() {
|
|
||||||
"--root" => {
|
|
||||||
parsed.root = Some(
|
|
||||||
iter.next()
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.ok_or_else(|| "--root requires a path".to_string())?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
"--mission" => {
|
|
||||||
parsed.mission = Some(
|
|
||||||
iter.next()
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| "--mission requires a path".to_string())?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
"--ticks" => {
|
|
||||||
parsed.ticks = iter
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| "--ticks requires a value".to_string())?
|
|
||||||
.parse()
|
|
||||||
.map_err(|_| "--ticks must be an integer".to_string())?;
|
|
||||||
}
|
|
||||||
"--move-object" => {
|
|
||||||
if parsed.reference_movement.is_some() {
|
|
||||||
return Err("--move-object may be specified once".to_string());
|
|
||||||
}
|
|
||||||
let original_id = iter
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| "--move-object requires an original object id".to_string())?
|
|
||||||
.parse()
|
|
||||||
.map_err(|_| {
|
|
||||||
"--move-object object id must be an unsigned integer".to_string()
|
|
||||||
})?;
|
|
||||||
let x = parse_finite_argument(
|
|
||||||
iter.next(),
|
|
||||||
"--move-object requires a finite X target",
|
|
||||||
)?;
|
|
||||||
let y = parse_finite_argument(
|
|
||||||
iter.next(),
|
|
||||||
"--move-object requires a finite Y target",
|
|
||||||
)?;
|
|
||||||
let max_step = parse_finite_argument(
|
|
||||||
iter.next(),
|
|
||||||
"--move-object requires a finite positive maximum step",
|
|
||||||
)?;
|
|
||||||
if max_step <= 0.0 {
|
|
||||||
return Err("--move-object maximum step must be positive".to_string());
|
|
||||||
}
|
|
||||||
parsed.reference_movement = Some(ReferenceMovement {
|
|
||||||
original_id,
|
|
||||||
target_xy: [x, y],
|
|
||||||
max_step,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_ => return Err(usage()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if parsed.mission.is_some() && parsed.root.is_none() {
|
|
||||||
return Err("--mission requires --root".to_string());
|
|
||||||
}
|
|
||||||
if parsed.reference_movement.is_some() && parsed.mission.is_none() {
|
|
||||||
return Err("--move-object requires --mission".to_string());
|
|
||||||
}
|
|
||||||
Ok(parsed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_finite_argument(value: Option<&String>, error: &str) -> Result<f32, String> {
|
|
||||||
let value: f32 = value
|
|
||||||
.ok_or_else(|| error.to_string())?
|
|
||||||
.parse()
|
|
||||||
.map_err(|_| error.to_string())?;
|
|
||||||
value
|
|
||||||
.is_finite()
|
|
||||||
.then_some(value)
|
|
||||||
.ok_or_else(|| error.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn usage() -> String {
|
|
||||||
"usage: fparkan-headless [--root <path> --mission <path>] [--move-object <original-id> <x> <y> <max-step>] [--ticks <n>]".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn args(values: &[&str]) -> Vec<String> {
|
|
||||||
values.iter().map(ToString::to_string).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn move_object_parses_a_single_finite_reference_command() {
|
|
||||||
let parsed = Args::parse(&args(&[
|
|
||||||
"--root",
|
|
||||||
"C:/game",
|
|
||||||
"--mission",
|
|
||||||
"MISSIONS/Autodemo.00/data.tma",
|
|
||||||
"--move-object",
|
|
||||||
"7",
|
|
||||||
"12.5",
|
|
||||||
"-3",
|
|
||||||
"0.25",
|
|
||||||
"--ticks",
|
|
||||||
"2",
|
|
||||||
]))
|
|
||||||
.expect("args");
|
|
||||||
assert_eq!(parsed.ticks, 2);
|
|
||||||
assert_eq!(
|
|
||||||
parsed.reference_movement,
|
|
||||||
Some(ReferenceMovement {
|
|
||||||
original_id: 7,
|
|
||||||
target_xy: [12.5, -3.0],
|
|
||||||
max_step: 0.25,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn move_object_requires_a_loaded_mission_and_valid_step() {
|
|
||||||
assert_eq!(
|
|
||||||
Args::parse(&args(&["--move-object", "7", "1", "2", "1"])).expect_err("mission"),
|
|
||||||
"--move-object requires --mission"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Args::parse(&args(&[
|
|
||||||
"--root",
|
|
||||||
"C:/game",
|
|
||||||
"--mission",
|
|
||||||
"M/data.tma",
|
|
||||||
"--move-object",
|
|
||||||
"7",
|
|
||||||
"1",
|
|
||||||
"2",
|
|
||||||
"0",
|
|
||||||
]))
|
|
||||||
.expect_err("step"),
|
|
||||||
"--move-object maximum step must be positive"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "fparkan-viewer"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
repository.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
fparkan-inspection = { path = "../../crates/fparkan-inspection", version = "0.1.0" }
|
|
||||||
fparkan-render = { path = "../../crates/fparkan-render", version = "0.1.0" }
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
@@ -1,350 +0,0 @@
|
|||||||
#![forbid(unsafe_code)]
|
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
#![allow(clippy::print_stderr, clippy::print_stdout)]
|
|
||||||
//! `FParkan` asset inspection composition root.
|
|
||||||
|
|
||||||
use fparkan_inspection::{
|
|
||||||
inspect_land_file, inspect_model_from_root, inspect_texture_from_root, ArchiveInspection,
|
|
||||||
LandFileKind, MapInspection, NresEntrySummary,
|
|
||||||
};
|
|
||||||
use fparkan_render::{
|
|
||||||
build_commands, CameraSnapshot, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderPhase,
|
|
||||||
RenderProfile, RenderSnapshot, RenderSnapshotDraw,
|
|
||||||
};
|
|
||||||
use std::fmt::Write;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let args = std::env::args().skip(1).collect::<Vec<_>>();
|
|
||||||
let code = match run(&args) {
|
|
||||||
Ok(json) => {
|
|
||||||
println!("{json}");
|
|
||||||
0
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("{err}");
|
|
||||||
2
|
|
||||||
}
|
|
||||||
};
|
|
||||||
std::process::exit(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(args: &[String]) -> Result<String, String> {
|
|
||||||
match args {
|
|
||||||
[domain, rest @ ..] if domain == "archive" => inspect_archive(rest),
|
|
||||||
[domain, rest @ ..] if domain == "model" => inspect_model(rest),
|
|
||||||
[domain, rest @ ..] if domain == "texture" => inspect_texture(rest),
|
|
||||||
[domain, rest @ ..] if domain == "map" => inspect_map(rest),
|
|
||||||
_ => Err(usage()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inspect_archive(args: &[String]) -> Result<String, String> {
|
|
||||||
let file = parse_file(args)?;
|
|
||||||
let limit = parse_limit(args)?;
|
|
||||||
let inspection = fparkan_inspection::inspect_archive_file(&file, limit)?;
|
|
||||||
|
|
||||||
match inspection {
|
|
||||||
ArchiveInspection::Nres {
|
|
||||||
entries,
|
|
||||||
lookup_order_valid,
|
|
||||||
sample,
|
|
||||||
} => Ok(format!(
|
|
||||||
"{{\"report_kind\":\"archive-inspection\",\"kind\":\"NRes\",\"path\":{},\"entries\":{},\"lookup_order_valid\":{},\"sample\":[{}]}}",
|
|
||||||
json_string(&file.display().to_string()),
|
|
||||||
entries,
|
|
||||||
lookup_order_valid,
|
|
||||||
render_nres_entries(&sample)
|
|
||||||
)),
|
|
||||||
ArchiveInspection::Rsli { entries } => Ok(format!(
|
|
||||||
"{{\"report_kind\":\"archive-inspection\",\"kind\":\"RsLi\",\"path\":{},\"entries\":{}}}",
|
|
||||||
json_string(&file.display().to_string()),
|
|
||||||
entries
|
|
||||||
)),
|
|
||||||
ArchiveInspection::Unsupported => Err(format!("{}: unsupported archive magic", file.display())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inspect_model(args: &[String]) -> Result<String, String> {
|
|
||||||
if let Some(fixture) = parse_option(args, &["--fixture"]) {
|
|
||||||
return ViewerModelService::inspect_synthetic_model(&fixture);
|
|
||||||
}
|
|
||||||
|
|
||||||
let query = parse_resource_query(args)?;
|
|
||||||
let inspection = inspect_model_from_root(&query.root, &query.archive, &query.name)?;
|
|
||||||
|
|
||||||
Ok(format!(
|
|
||||||
"{{\"report_kind\":\"model-inspection\",\"kind\":\"model\",\"archive\":{},\"name\":{},\"streams\":{},\"nodes\":{},\"slots\":{},\"positions\":{},\"indices\":{},\"batches\":{}}}",
|
|
||||||
json_string(&query.archive),
|
|
||||||
json_string(&query.name),
|
|
||||||
inspection.streams,
|
|
||||||
inspection.nodes,
|
|
||||||
inspection.slots,
|
|
||||||
inspection.positions,
|
|
||||||
inspection.indices,
|
|
||||||
inspection.batches
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
struct ViewerModelService;
|
|
||||||
|
|
||||||
impl ViewerModelService {
|
|
||||||
fn inspect_synthetic_model(fixture: &str) -> Result<String, String> {
|
|
||||||
if fixture != "synthetic/model-basic" {
|
|
||||||
return Err(format!("unknown model fixture: {fixture}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let snapshot = RenderSnapshot {
|
|
||||||
camera: CameraSnapshot::default(),
|
|
||||||
draws: vec![RenderSnapshotDraw {
|
|
||||||
id: DrawId(1),
|
|
||||||
phase: RenderPhase::Opaque,
|
|
||||||
object_id: None,
|
|
||||||
mesh: GpuMeshId(1),
|
|
||||||
material_slots: vec![GpuMaterialId(7)],
|
|
||||||
material_index: 0,
|
|
||||||
pipeline_state: fparkan_render::LegacyPipelineState::default(),
|
|
||||||
transform: identity_transform(),
|
|
||||||
range: IndexRange { start: 0, count: 3 },
|
|
||||||
stable_order: 0,
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
let commands = build_commands(&snapshot, RenderProfile::default())
|
|
||||||
.map_err(|err| format!("render command generation: {err}"))?;
|
|
||||||
let draw_commands = commands
|
|
||||||
.commands
|
|
||||||
.iter()
|
|
||||||
.filter(|command| matches!(command, fparkan_render::RenderCommand::Draw(_)))
|
|
||||||
.count();
|
|
||||||
|
|
||||||
Ok(format!(
|
|
||||||
"{{\"report_kind\":\"model-inspection\",\"kind\":\"model\",\"fixture\":{},\"service\":\"synthetic-model-inspection\",\"draw_commands\":{draw_commands}}}",
|
|
||||||
json_string(fixture)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inspect_texture(args: &[String]) -> Result<String, String> {
|
|
||||||
let query = parse_resource_query(args)?;
|
|
||||||
let inspection = inspect_texture_from_root(&query.root, &query.archive, &query.name)?;
|
|
||||||
|
|
||||||
Ok(format!(
|
|
||||||
"{{\"report_kind\":\"texture-inspection\",\"kind\":\"texture\",\"archive\":{},\"name\":{},\"width\":{},\"height\":{},\"format\":{},\"mips\":{},\"pages\":{}}}",
|
|
||||||
json_string(&query.archive),
|
|
||||||
json_string(&query.name),
|
|
||||||
inspection.width,
|
|
||||||
inspection.height,
|
|
||||||
json_string(&inspection.format),
|
|
||||||
inspection.mips,
|
|
||||||
inspection.pages
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inspect_map(args: &[String]) -> Result<String, String> {
|
|
||||||
let file = parse_file(args)?;
|
|
||||||
let kind = parse_option(args, &["--kind"]).ok_or_else(|| "missing --kind".to_string())?;
|
|
||||||
let inspection = inspect_land_file(
|
|
||||||
&file,
|
|
||||||
match kind.as_str() {
|
|
||||||
"land-msh" => LandFileKind::LandMsh,
|
|
||||||
"land-map" => LandFileKind::LandMap,
|
|
||||||
_ => return Err(format!("unknown map kind: {kind}")),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(render_map_inspection_json(
|
|
||||||
&file.display().to_string(),
|
|
||||||
&kind,
|
|
||||||
&inspection,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_map_inspection_json(path: &str, kind: &str, inspection: &MapInspection) -> String {
|
|
||||||
match kind {
|
|
||||||
"land-msh" => format!(
|
|
||||||
"{{\"report_kind\":\"map-inspection\",\"kind\":\"land-msh\",\"path\":{},\"streams\":{},\"positions\":{},\"faces\":{},\"slots\":{}}}",
|
|
||||||
json_string(path),
|
|
||||||
inspection.streams,
|
|
||||||
inspection.positions,
|
|
||||||
inspection.faces,
|
|
||||||
inspection.slots
|
|
||||||
),
|
|
||||||
"land-map" => format!(
|
|
||||||
"{{\"report_kind\":\"map-inspection\",\"kind\":\"land-map\",\"path\":{},\"areals\":{},\"declared_areals\":{},\"grid_width\":{},\"grid_height\":{}}}",
|
|
||||||
json_string(path),
|
|
||||||
inspection.areals,
|
|
||||||
inspection.declared_areals,
|
|
||||||
inspection.grid_width,
|
|
||||||
inspection.grid_height
|
|
||||||
),
|
|
||||||
_ => unreachable!("invalid land kind: {kind}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ResourceQuery {
|
|
||||||
root: PathBuf,
|
|
||||||
archive: String,
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_resource_query(args: &[String]) -> Result<ResourceQuery, String> {
|
|
||||||
Ok(ResourceQuery {
|
|
||||||
root: parse_path_option(args, &["--root", "--game-root"], "--root")?,
|
|
||||||
archive: parse_option(args, &["--archive"])
|
|
||||||
.ok_or_else(|| "missing --archive".to_string())?,
|
|
||||||
name: parse_option(args, &["--name"]).ok_or_else(|| "missing --name".to_string())?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_file(args: &[String]) -> Result<PathBuf, String> {
|
|
||||||
parse_path_option(args, &["--file"], "--file")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_limit(args: &[String]) -> Result<usize, String> {
|
|
||||||
parse_option(args, &["--limit"])
|
|
||||||
.map(|value| {
|
|
||||||
value
|
|
||||||
.parse::<usize>()
|
|
||||||
.map_err(|_| format!("invalid --limit: {value}"))
|
|
||||||
})
|
|
||||||
.transpose()
|
|
||||||
.map(|value| value.unwrap_or(0))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_nres_entries(entries: &[NresEntrySummary]) -> String {
|
|
||||||
let mut out = String::new();
|
|
||||||
for (index, entry) in entries.iter().enumerate() {
|
|
||||||
if index > 0 {
|
|
||||||
out.push(',');
|
|
||||||
}
|
|
||||||
let name = &entry.name;
|
|
||||||
let _ = write!(
|
|
||||||
out,
|
|
||||||
"{{\"name\":{},\"type\":{},\"size\":{}}}",
|
|
||||||
json_string(name),
|
|
||||||
entry.type_id,
|
|
||||||
entry.data_size
|
|
||||||
);
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_path_option(args: &[String], names: &[&str], label: &str) -> Result<PathBuf, String> {
|
|
||||||
parse_option(args, names)
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.ok_or_else(|| format!("missing {label}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_option(args: &[String], names: &[&str]) -> Option<String> {
|
|
||||||
let mut iter = args.iter();
|
|
||||||
while let Some(arg) = iter.next() {
|
|
||||||
if names.iter().any(|name| arg == name) {
|
|
||||||
return iter.next().cloned();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn json_string(value: &str) -> String {
|
|
||||||
let mut out = String::with_capacity(value.len() + 2);
|
|
||||||
out.push('"');
|
|
||||||
for ch in value.chars() {
|
|
||||||
match ch {
|
|
||||||
'"' => out.push_str("\\\""),
|
|
||||||
'\\' => out.push_str("\\\\"),
|
|
||||||
'\n' => out.push_str("\\n"),
|
|
||||||
'\r' => out.push_str("\\r"),
|
|
||||||
'\t' => out.push_str("\\t"),
|
|
||||||
c if c.is_control() => {
|
|
||||||
let _ = write!(out, "\\u{:04x}", c as u32);
|
|
||||||
}
|
|
||||||
c => out.push(c),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out.push('"');
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
fn identity_transform() -> [f32; 16] {
|
|
||||||
[
|
|
||||||
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn usage() -> String {
|
|
||||||
"usage: fparkan-viewer archive --file <archive> [--limit N] | model --root <game-root> --archive <archive> --name <msh> | model --fixture synthetic/model-basic | texture --root <game-root> --archive <archive> --name <texm> | map --file <Land.msh|Land.map> --kind land-msh|land-map".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn strings(values: &[&str]) -> Vec<String> {
|
|
||||||
values.iter().map(|value| (*value).to_string()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parses_resource_query() -> Result<(), String> {
|
|
||||||
let query = parse_resource_query(&strings(&[
|
|
||||||
"--root",
|
|
||||||
"testdata/IS",
|
|
||||||
"--archive",
|
|
||||||
"textures.lib",
|
|
||||||
"--name",
|
|
||||||
"grass.tex",
|
|
||||||
]))?;
|
|
||||||
|
|
||||||
assert_eq!(query.root, PathBuf::from("testdata/IS"));
|
|
||||||
assert_eq!(query.archive, "textures.lib");
|
|
||||||
assert_eq!(query.name, "grass.tex");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn json_string_escapes_controls() {
|
|
||||||
assert_eq!(json_string("a\"b\\c\n"), "\"a\\\"b\\\\c\\n\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn usage_rejects_empty_args() {
|
|
||||||
assert_eq!(run(&[]), Err(usage()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parses_limit() {
|
|
||||||
assert_eq!(parse_limit(&strings(&["--limit", "2"])), Ok(2));
|
|
||||||
assert_eq!(parse_limit(&[]), Ok(0));
|
|
||||||
assert_eq!(
|
|
||||||
parse_limit(&strings(&["--limit", "x"])),
|
|
||||||
Err("invalid --limit: x".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn model_fixture_uses_viewer_service_and_render_commands() -> Result<(), String> {
|
|
||||||
assert_eq!(
|
|
||||||
run(&strings(&["model", "--fixture", "synthetic/model-basic"]))?,
|
|
||||||
"{\"report_kind\":\"model-inspection\",\"kind\":\"model\",\"fixture\":\"synthetic/model-basic\",\"service\":\"synthetic-model-inspection\",\"draw_commands\":1}"
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "fparkan-vulkan-smoke"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
repository.workspace = true
|
|
||||||
build = "build.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
fparkan-platform = { path = "../../crates/fparkan-platform", version = "0.1.0" }
|
|
||||||
fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit", version = "0.1.0" }
|
|
||||||
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan", version = "0.1.0" }
|
|
||||||
fparkan-inspection = { path = "../../crates/fparkan-inspection", version = "0.1.0" }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
winit = { version = "0.30", default-features = false, features = ["rwh_06"] }
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
//! Build-time provenance for native smoke artifacts.
|
|
||||||
|
|
||||||
use std::env;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
println!("cargo:rerun-if-env-changed=TARGET");
|
|
||||||
println!("cargo:rerun-if-env-changed=RUSTC");
|
|
||||||
println!("cargo:rerun-if-env-changed=RUSTUP_TOOLCHAIN");
|
|
||||||
println!("cargo:rerun-if-env-changed=GITHUB_SHA");
|
|
||||||
println!("cargo:rerun-if-env-changed=SOURCE_VERSION");
|
|
||||||
println!("cargo:rerun-if-env-changed=BUILD_VCS_NUMBER");
|
|
||||||
|
|
||||||
if let Ok(target) = env::var("TARGET") {
|
|
||||||
println!("cargo:rustc-env=FPARKAN_BUILD_TARGET_TRIPLE={target}");
|
|
||||||
}
|
|
||||||
if let Some(toolchain) = rustc_release() {
|
|
||||||
println!("cargo:rustc-env=FPARKAN_BUILD_RUST_TOOLCHAIN={toolchain}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(workspace_root) = workspace_root() {
|
|
||||||
if let Some(git_dir) = git_dir(&workspace_root) {
|
|
||||||
emit_git_rerun_hints(&git_dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(commit_sha) = env_commit_sha().or_else(|| git_head_commit_sha(&workspace_root))
|
|
||||||
{
|
|
||||||
println!("cargo:rustc-env=FPARKAN_BUILD_COMMIT_SHA={commit_sha}");
|
|
||||||
}
|
|
||||||
if let Some(git_dirty) = git_dirty(&workspace_root) {
|
|
||||||
println!("cargo:rustc-env=FPARKAN_BUILD_GIT_DIRTY={git_dirty}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn workspace_root() -> Option<PathBuf> {
|
|
||||||
env::var_os("CARGO_MANIFEST_DIR")
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.map(|manifest_dir| manifest_dir.join("../.."))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn env_commit_sha() -> Option<String> {
|
|
||||||
["GITHUB_SHA", "SOURCE_VERSION", "BUILD_VCS_NUMBER"]
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|name| env::var(name).ok())
|
|
||||||
.find(|value| is_commit_sha(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn git_head_commit_sha(workspace_root: &Path) -> Option<String> {
|
|
||||||
let output = Command::new("git")
|
|
||||||
.args(["-C"])
|
|
||||||
.arg(workspace_root)
|
|
||||||
.args(["rev-parse", "HEAD"])
|
|
||||||
.output()
|
|
||||||
.ok()?;
|
|
||||||
if !output.status.success() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let value = String::from_utf8(output.stdout).ok()?;
|
|
||||||
let value = value.trim().to_string();
|
|
||||||
is_commit_sha(&value).then_some(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn git_dirty(workspace_root: &Path) -> Option<bool> {
|
|
||||||
let output = Command::new("git")
|
|
||||||
.args(["-C"])
|
|
||||||
.arg(workspace_root)
|
|
||||||
.args(["status", "--short"])
|
|
||||||
.output()
|
|
||||||
.ok()?;
|
|
||||||
output
|
|
||||||
.status
|
|
||||||
.success()
|
|
||||||
.then(|| !String::from_utf8_lossy(&output.stdout).trim().is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn git_dir(workspace_root: &Path) -> Option<PathBuf> {
|
|
||||||
let output = Command::new("git")
|
|
||||||
.args(["-C"])
|
|
||||||
.arg(workspace_root)
|
|
||||||
.args(["rev-parse", "--git-dir"])
|
|
||||||
.output()
|
|
||||||
.ok()?;
|
|
||||||
if !output.status.success() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let value = String::from_utf8(output.stdout).ok()?;
|
|
||||||
let value = value.trim();
|
|
||||||
(!value.is_empty()).then(|| workspace_root.join(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_git_rerun_hints(git_dir: &Path) {
|
|
||||||
let head = git_dir.join("HEAD");
|
|
||||||
println!("cargo:rerun-if-changed={}", head.display());
|
|
||||||
println!(
|
|
||||||
"cargo:rerun-if-changed={}",
|
|
||||||
git_dir.join("packed-refs").display()
|
|
||||||
);
|
|
||||||
println!("cargo:rerun-if-changed={}", git_dir.join("index").display());
|
|
||||||
let Some(reference) = std::fs::read_to_string(&head).ok().and_then(|value| {
|
|
||||||
value
|
|
||||||
.strip_prefix("ref: ")
|
|
||||||
.map(str::trim)
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
}) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
println!(
|
|
||||||
"cargo:rerun-if-changed={}",
|
|
||||||
git_dir.join(reference).display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_commit_sha(value: &str) -> bool {
|
|
||||||
value.len() == 40 && value.chars().all(|ch| ch.is_ascii_hexdigit())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rustc_release() -> Option<String> {
|
|
||||||
let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string());
|
|
||||||
let output = Command::new(rustc).arg("-Vv").output().ok()?;
|
|
||||||
if !output.status.success() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
String::from_utf8(output.stdout)
|
|
||||||
.ok()?
|
|
||||||
.lines()
|
|
||||||
.find_map(|line| {
|
|
||||||
line.strip_prefix("release: ")
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
.map(ToString::to_string)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,3 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,23 +1,5 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
#![allow(clippy::cast_precision_loss)]
|
|
||||||
//! Deterministic animation sampling contracts.
|
//! Deterministic animation sampling contracts.
|
||||||
//!
|
//!
|
||||||
//! The current sampler is a portable reference path. Compatibility profiles
|
//! The current sampler is a portable reference path. Compatibility profiles
|
||||||
|
|||||||
@@ -6,19 +6,14 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fparkan-material = { path = "../fparkan-material", version = "0.1.0" }
|
fparkan-material = { path = "../fparkan-material" }
|
||||||
fparkan-msh = { path = "../fparkan-msh", version = "0.1.0" }
|
fparkan-msh = { path = "../fparkan-msh" }
|
||||||
fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
|
fparkan-nres = { path = "../fparkan-nres" }
|
||||||
fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
|
fparkan-path = { path = "../fparkan-path" }
|
||||||
fparkan-mission-format = { path = "../fparkan-mission-format", version = "0.1.0" }
|
fparkan-mission-format = { path = "../fparkan-mission-format" }
|
||||||
fparkan-prototype = { path = "../fparkan-prototype", version = "0.1.0" }
|
fparkan-prototype = { path = "../fparkan-prototype" }
|
||||||
fparkan-resource = { path = "../fparkan-resource", version = "0.1.0" }
|
fparkan-resource = { path = "../fparkan-resource" }
|
||||||
fparkan-texm = { path = "../fparkan-texm", version = "0.1.0" }
|
fparkan-texm = { path = "../fparkan-texm" }
|
||||||
fparkan-terrain = { path = "../fparkan-terrain", version = "0.1.0" }
|
fparkan-terrain = { path = "../fparkan-terrain" }
|
||||||
fparkan-terrain-format = { path = "../fparkan-terrain-format", version = "0.1.0" }
|
fparkan-terrain-format = { path = "../fparkan-terrain-format" }
|
||||||
|
fparkan-vfs = { path = "../fparkan-vfs" }
|
||||||
[dev-dependencies]
|
|
||||||
fparkan-vfs = { path = "../fparkan-vfs", version = "0.1.0" }
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Asset manager ports and transactional preparation models.
|
//! Asset manager ports and transactional preparation models.
|
||||||
|
|
||||||
use fparkan_material::{
|
use fparkan_material::{
|
||||||
@@ -35,24 +16,24 @@ use fparkan_prototype::{
|
|||||||
PrototypeGraphFailure, PrototypeGraphNodeKind, PrototypeGraphProvenance, PrototypeGraphReport,
|
PrototypeGraphFailure, PrototypeGraphNodeKind, PrototypeGraphProvenance, PrototypeGraphReport,
|
||||||
PrototypeGraphRequiredness,
|
PrototypeGraphRequiredness,
|
||||||
};
|
};
|
||||||
use fparkan_resource::{ResourceError, ResourceKey, ResourceRepository};
|
use fparkan_resource::{CachedResourceRepository, ResourceError, ResourceKey, ResourceRepository};
|
||||||
pub use fparkan_terrain::{TerrainError, TerrainWorld};
|
pub use fparkan_terrain::{TerrainError, TerrainWorld};
|
||||||
use fparkan_terrain_format::{decode_build_dat, decode_land_map, decode_land_msh};
|
use fparkan_terrain_format::{decode_build_dat, decode_land_map, decode_land_msh};
|
||||||
pub use fparkan_terrain_format::{BuildCategory, TerrainFormatError};
|
pub use fparkan_terrain_format::{BuildCategory, TerrainFormatError};
|
||||||
use fparkan_texm::{decode_mip_rgba8, decode_texm, RgbaImage, TexmDocument, TexmError};
|
use fparkan_texm::{decode_mip_rgba8, decode_texm, RgbaImage, TexmDocument, TexmError};
|
||||||
|
use fparkan_vfs::DirectoryVfs;
|
||||||
use std::collections::{hash_map::Entry, HashMap, HashSet};
|
use std::collections::{hash_map::Entry, HashMap, HashSet};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
const TEXTURES_ARCHIVE: &str = "textures.lib";
|
const TEXTURES_ARCHIVE: &str = "textures.lib";
|
||||||
const LIGHTMAP_ARCHIVE: &str = "lightmap.lib";
|
const LIGHTMAP_ARCHIVE: &str = "lightmap.lib";
|
||||||
const VISUAL_DEPENDENCY_PROGRESS_INTERVAL: usize = 64;
|
|
||||||
|
|
||||||
type WearValidationCache =
|
type WearValidationCache =
|
||||||
HashMap<(NormalizedPath, Vec<u8>), Result<fparkan_material::WearTable, String>>;
|
HashMap<(NormalizedPath, Vec<u8>), Result<fparkan_material::WearTable, String>>;
|
||||||
type TextureValidationCache = HashMap<Vec<u8>, Result<(), String>>;
|
|
||||||
|
|
||||||
/// Canonical terrain archive paths derived from a mission land reference.
|
/// Canonical terrain archive paths derived from a mission land reference.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
@@ -188,6 +169,37 @@ pub fn decode_nres_payload(
|
|||||||
decode_nres(bytes, ReadProfile::Compatible)
|
decode_nres(bytes, ReadProfile::Compatible)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loads a phase-zero diffuse texture from a map-local standalone WEAR file.
|
||||||
|
///
|
||||||
|
/// The sidecar stays beside `Land.msh`; MAT0 and TEXM remain in the normal
|
||||||
|
/// game-root archives.
|
||||||
|
pub fn load_standalone_wear_material_texture_mip0_rgba8_from_root(
|
||||||
|
root: &Path,
|
||||||
|
wear_path: &Path,
|
||||||
|
material_index: u16,
|
||||||
|
) -> Result<RgbaImage, String> {
|
||||||
|
let wear = decode_wear(
|
||||||
|
&std::fs::read(wear_path).map_err(|err| format!("{}: {err}", wear_path.display()))?,
|
||||||
|
)
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
let repository = CachedResourceRepository::new(Arc::new(DirectoryVfs::new(root)));
|
||||||
|
let material =
|
||||||
|
resolve_material(&repository, &wear, material_index).map_err(|err| err.to_string())?;
|
||||||
|
let texture = material.document.primary_texture().ok_or_else(|| {
|
||||||
|
"MAT0 phase zero declares an intentionally untextured material".to_string()
|
||||||
|
})?;
|
||||||
|
let key = ResourceKey {
|
||||||
|
archive: parse_path(TEXTURES_ARCHIVE).map_err(|err| err.to_string())?,
|
||||||
|
name: texture.clone(),
|
||||||
|
type_id: None,
|
||||||
|
};
|
||||||
|
let bytes = read_optional_key(&repository, &key, Some("texture"))
|
||||||
|
.map_err(|err| err.to_string())?
|
||||||
|
.ok_or_else(|| format!("missing texture {texture:?}"))?;
|
||||||
|
let document = decode_texm(bytes).map_err(|err| err.to_string())?;
|
||||||
|
decode_mip_rgba8(&document, 0).map_err(|err| err.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Decodes terrain documents and builds immutable terrain state.
|
/// Decodes terrain documents and builds immutable terrain state.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
@@ -494,56 +506,6 @@ pub struct AssetPreparationReport {
|
|||||||
pub texture_pixels: u64,
|
pub texture_pixels: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Asset preparation checkpoint emitted while resolving visual dependencies.
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
pub enum AssetPreparationPhase {
|
|
||||||
/// Decode and validate an MSH model.
|
|
||||||
ModelMesh,
|
|
||||||
/// Decode the corresponding WEAR table.
|
|
||||||
WearTable,
|
|
||||||
/// Resolve MAT0 material documents referenced by a WEAR table.
|
|
||||||
Materials,
|
|
||||||
/// Decode diffuse textures and baked lightmaps.
|
|
||||||
Textures,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Visual dependency class currently being expanded into a prototype graph.
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
pub enum VisualDependencyPhase {
|
|
||||||
/// Resolve a mesh's WEAR table.
|
|
||||||
Wear,
|
|
||||||
/// Resolve MAT0 material documents selected by WEAR.
|
|
||||||
Material,
|
|
||||||
/// Validate diffuse TEXM textures and lightmaps.
|
|
||||||
Texture,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validation-cache entries retained while expanding visual dependencies.
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
||||||
pub struct VisualDependencyCacheCounts {
|
|
||||||
/// Distinct WEAR archive/name keys resolved so far.
|
|
||||||
pub wear_entries: usize,
|
|
||||||
/// Distinct MAT0 raw names resolved so far.
|
|
||||||
pub material_entries: usize,
|
|
||||||
/// Distinct TEXM archive/name keys resolved so far.
|
|
||||||
pub texture_entries: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Throttled visual-dependency expansion progress.
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
pub struct VisualDependencyProgress {
|
|
||||||
/// Dependency class currently being expanded.
|
|
||||||
pub phase: VisualDependencyPhase,
|
|
||||||
/// Number of requests reached in this class during the current expansion.
|
|
||||||
pub request_count: usize,
|
|
||||||
/// Resource graph nodes materialized before this request is resolved.
|
|
||||||
pub graph_node_count: usize,
|
|
||||||
/// Resource graph edges materialized before this request is resolved.
|
|
||||||
pub graph_edge_count: usize,
|
|
||||||
/// Validation-cache entries retained before this request is resolved.
|
|
||||||
pub cache_entries: VisualDependencyCacheCounts,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Errors raised while preparing CPU-side assets.
|
/// Errors raised while preparing CPU-side assets.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum AssetError {
|
pub enum AssetError {
|
||||||
@@ -660,25 +622,6 @@ impl<R: ResourceRepository> AssetManager<R> {
|
|||||||
prepare_mission_assets_with_repository(&self.repository, root_prototype_spans, prototypes)
|
prepare_mission_assets_with_repository(&self.repository, root_prototype_spans, prototypes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds mission assets and reports the active dependency class.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns [`AssetError`] if any visual dependency is missing or malformed.
|
|
||||||
pub fn prepare_mission_assets_with_progress(
|
|
||||||
&self,
|
|
||||||
root_prototype_spans: &[std::ops::Range<usize>],
|
|
||||||
prototypes: &[EffectivePrototype],
|
|
||||||
on_phase: impl FnMut(AssetPreparationPhase),
|
|
||||||
) -> Result<MissionAssets, AssetError> {
|
|
||||||
prepare_mission_assets_with_repository_with_progress(
|
|
||||||
&self.repository,
|
|
||||||
root_prototype_spans,
|
|
||||||
prototypes,
|
|
||||||
on_phase,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds mission assets together with a bounded preparation report.
|
/// Builds mission assets together with a bounded preparation report.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
@@ -764,28 +707,6 @@ pub fn prepare_mission_assets_with_repository<R: ResourceRepository>(
|
|||||||
.0)
|
.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds mission assets and reports the active dependency class.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns [`AssetError`] if any visual dependency is missing or malformed.
|
|
||||||
pub fn prepare_mission_assets_with_repository_with_progress<R: ResourceRepository>(
|
|
||||||
repository: &R,
|
|
||||||
root_prototype_spans: &[std::ops::Range<usize>],
|
|
||||||
prototypes: &[EffectivePrototype],
|
|
||||||
mut on_phase: impl FnMut(AssetPreparationPhase),
|
|
||||||
) -> Result<MissionAssets, AssetError> {
|
|
||||||
Ok(prepare_mission_assets_with_repository_internal(
|
|
||||||
repository,
|
|
||||||
root_prototype_spans,
|
|
||||||
prototypes,
|
|
||||||
AssetIdentityPolicy::default(),
|
|
||||||
AssetPreparationLimits::default(),
|
|
||||||
&mut on_phase,
|
|
||||||
)?
|
|
||||||
.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds mission assets while enforcing explicit preparation limits.
|
/// Builds mission assets while enforcing explicit preparation limits.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
@@ -798,27 +719,24 @@ pub fn prepare_mission_assets_profiled_with_repository<R: ResourceRepository>(
|
|||||||
prototypes: &[EffectivePrototype],
|
prototypes: &[EffectivePrototype],
|
||||||
limits: AssetPreparationLimits,
|
limits: AssetPreparationLimits,
|
||||||
) -> Result<(MissionAssets, AssetPreparationReport), AssetError> {
|
) -> Result<(MissionAssets, AssetPreparationReport), AssetError> {
|
||||||
let mut ignore_phase = |_| {};
|
|
||||||
prepare_mission_assets_with_repository_internal(
|
prepare_mission_assets_with_repository_internal(
|
||||||
repository,
|
repository,
|
||||||
root_prototype_spans,
|
root_prototype_spans,
|
||||||
prototypes,
|
prototypes,
|
||||||
AssetIdentityPolicy::default(),
|
AssetIdentityPolicy::default(),
|
||||||
limits,
|
limits,
|
||||||
&mut ignore_phase,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This orchestration intentionally keeps preparation, deduplication, and limit enforcement
|
// This orchestration intentionally keeps preparation, deduplication, and limit enforcement
|
||||||
// together so all fallible asset construction remains transactional.
|
// together so all fallible asset construction remains transactional.
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
fn prepare_mission_assets_with_repository_internal<R: ResourceRepository>(
|
fn prepare_mission_assets_with_repository_internal<R: ResourceRepository>(
|
||||||
repository: &R,
|
repository: &R,
|
||||||
root_prototype_spans: &[std::ops::Range<usize>],
|
root_prototype_spans: &[std::ops::Range<usize>],
|
||||||
prototypes: &[EffectivePrototype],
|
prototypes: &[EffectivePrototype],
|
||||||
identity_policy: AssetIdentityPolicy,
|
identity_policy: AssetIdentityPolicy,
|
||||||
limits: AssetPreparationLimits,
|
limits: AssetPreparationLimits,
|
||||||
on_phase: &mut dyn FnMut(AssetPreparationPhase),
|
|
||||||
) -> Result<(MissionAssets, AssetPreparationReport), AssetError> {
|
) -> Result<(MissionAssets, AssetPreparationReport), AssetError> {
|
||||||
if prototypes.is_empty() {
|
if prototypes.is_empty() {
|
||||||
return Ok((MissionAssets::default(), AssetPreparationReport::default()));
|
return Ok((MissionAssets::default(), AssetPreparationReport::default()));
|
||||||
@@ -858,7 +776,6 @@ fn prepare_mission_assets_with_repository_internal<R: ResourceRepository>(
|
|||||||
proto,
|
proto,
|
||||||
identity_policy,
|
identity_policy,
|
||||||
&mut preparation_cache,
|
&mut preparation_cache,
|
||||||
on_phase,
|
|
||||||
)?;
|
)?;
|
||||||
if bundle.visual.id != visual_id {
|
if bundle.visual.id != visual_id {
|
||||||
// Defensive check. stable IDs are deterministic for the same inputs.
|
// Defensive check. stable IDs are deterministic for the same inputs.
|
||||||
@@ -1173,73 +1090,17 @@ fn enforce_asset_preparation_limits(
|
|||||||
/// the path policy, which indicates a programming error in this module.
|
/// the path policy, which indicates a programming error in this module.
|
||||||
// The graph expansion is deliberately a single transactional traversal so every generated
|
// The graph expansion is deliberately a single transactional traversal so every generated
|
||||||
// edge receives the correct parent provenance and node/edge sequence.
|
// edge receives the correct parent provenance and node/edge sequence.
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
pub fn extend_graph_report_with_visual_dependencies<R: ResourceRepository>(
|
pub fn extend_graph_report_with_visual_dependencies<R: ResourceRepository>(
|
||||||
repository: &R,
|
repository: &R,
|
||||||
report: &mut PrototypeGraphReport,
|
report: &mut PrototypeGraphReport,
|
||||||
graph: &mut PrototypeGraph,
|
graph: &mut PrototypeGraph,
|
||||||
prototypes: &[EffectivePrototype],
|
prototypes: &[EffectivePrototype],
|
||||||
) {
|
|
||||||
extend_graph_report_with_visual_dependencies_and_progress(
|
|
||||||
repository,
|
|
||||||
report,
|
|
||||||
graph,
|
|
||||||
prototypes,
|
|
||||||
|_| {},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extends a prototype dependency report while reporting each visual dependency class.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// Panics only if the hard-coded, host-compatible `material.lib` path stops
|
|
||||||
/// satisfying the path policy, which indicates a programming error in this module.
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
pub fn extend_graph_report_with_visual_dependencies_and_progress<R: ResourceRepository>(
|
|
||||||
repository: &R,
|
|
||||||
report: &mut PrototypeGraphReport,
|
|
||||||
graph: &mut PrototypeGraph,
|
|
||||||
prototypes: &[EffectivePrototype],
|
|
||||||
mut on_phase: impl FnMut(VisualDependencyPhase),
|
|
||||||
) {
|
|
||||||
let mut last_phase = None;
|
|
||||||
extend_graph_report_with_visual_dependencies_with_progress(
|
|
||||||
repository,
|
|
||||||
report,
|
|
||||||
graph,
|
|
||||||
prototypes,
|
|
||||||
|progress| {
|
|
||||||
if last_phase != Some(progress.phase) {
|
|
||||||
on_phase(progress.phase);
|
|
||||||
last_phase = Some(progress.phase);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extends a prototype dependency report with throttled request progress.
|
|
||||||
///
|
|
||||||
/// The callback receives each dependency-class entrance, its first request,
|
|
||||||
/// and every subsequent multiple of 64 requests. Graph traversal, edge order
|
|
||||||
/// and validation remain unchanged.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// Panics only if the hard-coded, host-compatible `material.lib` path stops
|
|
||||||
/// satisfying the path policy, which indicates a programming error in this module.
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
pub fn extend_graph_report_with_visual_dependencies_with_progress<R: ResourceRepository>(
|
|
||||||
repository: &R,
|
|
||||||
report: &mut PrototypeGraphReport,
|
|
||||||
graph: &mut PrototypeGraph,
|
|
||||||
prototypes: &[EffectivePrototype],
|
|
||||||
mut on_progress: impl FnMut(VisualDependencyProgress),
|
|
||||||
) {
|
) {
|
||||||
if graph.visual_dependencies_expanded {
|
if graph.visual_dependencies_expanded {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#[allow(clippy::expect_used)]
|
|
||||||
let material_archive = parse_path("material.lib")
|
let material_archive = parse_path("material.lib")
|
||||||
.expect("static material archive path must satisfy host-compatible normalization");
|
.expect("static material archive path must satisfy host-compatible normalization");
|
||||||
let mut next_node = graph
|
let mut next_node = graph
|
||||||
@@ -1258,9 +1119,6 @@ pub fn extend_graph_report_with_visual_dependencies_with_progress<R: ResourceRep
|
|||||||
let mut lightmap_texture_validation = HashMap::new();
|
let mut lightmap_texture_validation = HashMap::new();
|
||||||
let mut material_validation = HashMap::new();
|
let mut material_validation = HashMap::new();
|
||||||
let mut wear_validation = HashMap::new();
|
let mut wear_validation = HashMap::new();
|
||||||
let mut wear_requests = 0usize;
|
|
||||||
let mut material_requests = 0usize;
|
|
||||||
let mut texture_requests = 0usize;
|
|
||||||
// Capture all base-graph anchors before this traversal appends visual
|
// Capture all base-graph anchors before this traversal appends visual
|
||||||
// resources and edges. Looking them up again from the growing graph would
|
// resources and edges. Looking them up again from the growing graph would
|
||||||
// make each later prototype scan an ever larger collection.
|
// make each later prototype scan an ever larger collection.
|
||||||
@@ -1288,22 +1146,6 @@ pub fn extend_graph_report_with_visual_dependencies_with_progress<R: ResourceRep
|
|||||||
let unit_component_index =
|
let unit_component_index =
|
||||||
mesh_parent_edge.and_then(|edge| unit_component_index_for_edge(graph, edge));
|
mesh_parent_edge.and_then(|edge| unit_component_index_for_edge(graph, edge));
|
||||||
|
|
||||||
wear_requests = wear_requests.saturating_add(1);
|
|
||||||
report_visual_dependency_progress(
|
|
||||||
&mut on_progress,
|
|
||||||
VisualDependencyProgress {
|
|
||||||
phase: VisualDependencyPhase::Wear,
|
|
||||||
request_count: wear_requests,
|
|
||||||
graph_node_count: graph.nodes.len(),
|
|
||||||
graph_edge_count: graph.edges.len(),
|
|
||||||
cache_entries: visual_dependency_cache_counts(
|
|
||||||
&wear_validation,
|
|
||||||
&material_validation,
|
|
||||||
&diffuse_texture_validation,
|
|
||||||
&lightmap_texture_validation,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
match resolve_wear_table_cached(repository, mesh, &mut wear_validation) {
|
match resolve_wear_table_cached(repository, mesh, &mut wear_validation) {
|
||||||
Ok(table) => {
|
Ok(table) => {
|
||||||
report.wear_resolved_count += 1;
|
report.wear_resolved_count += 1;
|
||||||
@@ -1343,35 +1185,7 @@ pub fn extend_graph_report_with_visual_dependencies_with_progress<R: ResourceRep
|
|||||||
&mut next_edge,
|
&mut next_edge,
|
||||||
);
|
);
|
||||||
report.material_slot_count += table.entries.len();
|
report.material_slot_count += table.entries.len();
|
||||||
on_progress(VisualDependencyProgress {
|
|
||||||
phase: VisualDependencyPhase::Material,
|
|
||||||
request_count: material_requests,
|
|
||||||
graph_node_count: graph.nodes.len(),
|
|
||||||
graph_edge_count: graph.edges.len(),
|
|
||||||
cache_entries: visual_dependency_cache_counts(
|
|
||||||
&wear_validation,
|
|
||||||
&material_validation,
|
|
||||||
&diffuse_texture_validation,
|
|
||||||
&lightmap_texture_validation,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
for (material_index, _entry) in table.entries.iter().enumerate() {
|
for (material_index, _entry) in table.entries.iter().enumerate() {
|
||||||
material_requests = material_requests.saturating_add(1);
|
|
||||||
report_visual_dependency_progress(
|
|
||||||
&mut on_progress,
|
|
||||||
VisualDependencyProgress {
|
|
||||||
phase: VisualDependencyPhase::Material,
|
|
||||||
request_count: material_requests,
|
|
||||||
graph_node_count: graph.nodes.len(),
|
|
||||||
graph_edge_count: graph.edges.len(),
|
|
||||||
cache_entries: visual_dependency_cache_counts(
|
|
||||||
&wear_validation,
|
|
||||||
&material_validation,
|
|
||||||
&diffuse_texture_validation,
|
|
||||||
&lightmap_texture_validation,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let Ok(material_index) = u16::try_from(material_index) else {
|
let Ok(material_index) = u16::try_from(material_index) else {
|
||||||
push_visual_failure(
|
push_visual_failure(
|
||||||
report,
|
report,
|
||||||
@@ -1418,22 +1232,6 @@ pub fn extend_graph_report_with_visual_dependencies_with_progress<R: ResourceRep
|
|||||||
&mut next_edge,
|
&mut next_edge,
|
||||||
);
|
);
|
||||||
for texture in material.document.texture_requests() {
|
for texture in material.document.texture_requests() {
|
||||||
texture_requests = texture_requests.saturating_add(1);
|
|
||||||
report_visual_dependency_progress(
|
|
||||||
&mut on_progress,
|
|
||||||
VisualDependencyProgress {
|
|
||||||
phase: VisualDependencyPhase::Texture,
|
|
||||||
request_count: texture_requests,
|
|
||||||
graph_node_count: graph.nodes.len(),
|
|
||||||
graph_edge_count: graph.edges.len(),
|
|
||||||
cache_entries: visual_dependency_cache_counts(
|
|
||||||
&wear_validation,
|
|
||||||
&material_validation,
|
|
||||||
&diffuse_texture_validation,
|
|
||||||
&lightmap_texture_validation,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
report.texture_request_count += 1;
|
report.texture_request_count += 1;
|
||||||
match resolve_texm_validation_cached(
|
match resolve_texm_validation_cached(
|
||||||
repository,
|
repository,
|
||||||
@@ -1495,22 +1293,6 @@ pub fn extend_graph_report_with_visual_dependencies_with_progress<R: ResourceRep
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for lightmap in &table.lightmaps {
|
for lightmap in &table.lightmaps {
|
||||||
texture_requests = texture_requests.saturating_add(1);
|
|
||||||
report_visual_dependency_progress(
|
|
||||||
&mut on_progress,
|
|
||||||
VisualDependencyProgress {
|
|
||||||
phase: VisualDependencyPhase::Texture,
|
|
||||||
request_count: texture_requests,
|
|
||||||
graph_node_count: graph.nodes.len(),
|
|
||||||
graph_edge_count: graph.edges.len(),
|
|
||||||
cache_entries: visual_dependency_cache_counts(
|
|
||||||
&wear_validation,
|
|
||||||
&material_validation,
|
|
||||||
&diffuse_texture_validation,
|
|
||||||
&lightmap_texture_validation,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
report.lightmap_request_count += 1;
|
report.lightmap_request_count += 1;
|
||||||
match resolve_texm_validation_cached(
|
match resolve_texm_validation_cached(
|
||||||
repository,
|
repository,
|
||||||
@@ -1574,34 +1356,6 @@ pub fn extend_graph_report_with_visual_dependencies_with_progress<R: ResourceRep
|
|||||||
graph.visual_dependencies_expanded = true;
|
graph.visual_dependencies_expanded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn report_visual_dependency_progress(
|
|
||||||
on_progress: &mut impl FnMut(VisualDependencyProgress),
|
|
||||||
progress: VisualDependencyProgress,
|
|
||||||
) {
|
|
||||||
if progress.request_count == 1
|
|
||||||
|| progress
|
|
||||||
.request_count
|
|
||||||
.is_multiple_of(VISUAL_DEPENDENCY_PROGRESS_INTERVAL)
|
|
||||||
{
|
|
||||||
on_progress(progress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visual_dependency_cache_counts(
|
|
||||||
wear_validation: &WearValidationCache,
|
|
||||||
material_validation: &HashMap<Vec<u8>, Result<ResolvedMaterial, String>>,
|
|
||||||
diffuse_texture_validation: &TextureValidationCache,
|
|
||||||
lightmap_texture_validation: &TextureValidationCache,
|
|
||||||
) -> VisualDependencyCacheCounts {
|
|
||||||
VisualDependencyCacheCounts {
|
|
||||||
wear_entries: wear_validation.len(),
|
|
||||||
material_entries: material_validation.len(),
|
|
||||||
texture_entries: diffuse_texture_validation
|
|
||||||
.len()
|
|
||||||
.saturating_add(lightmap_texture_validation.len()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_graph_resource_node(
|
fn push_graph_resource_node(
|
||||||
graph: &mut PrototypeGraph,
|
graph: &mut PrototypeGraph,
|
||||||
kind: PrototypeGraphNodeKind,
|
kind: PrototypeGraphNodeKind,
|
||||||
@@ -1738,7 +1492,6 @@ pub fn prepare_visual_with_repository<R: ResourceRepository>(
|
|||||||
proto,
|
proto,
|
||||||
AssetIdentityPolicy::default(),
|
AssetIdentityPolicy::default(),
|
||||||
&mut preparation_cache,
|
&mut preparation_cache,
|
||||||
&mut |_| {},
|
|
||||||
)?
|
)?
|
||||||
.visual)
|
.visual)
|
||||||
}
|
}
|
||||||
@@ -1761,13 +1514,12 @@ struct PreparationCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Keeps the complete model -> WEAR -> material -> texture transaction in one fallible boundary.
|
// Keeps the complete model -> WEAR -> material -> texture transaction in one fallible boundary.
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
fn prepare_visual_with_repository_internal<R: ResourceRepository>(
|
fn prepare_visual_with_repository_internal<R: ResourceRepository>(
|
||||||
repository: &R,
|
repository: &R,
|
||||||
proto: &EffectivePrototype,
|
proto: &EffectivePrototype,
|
||||||
identity_policy: AssetIdentityPolicy,
|
identity_policy: AssetIdentityPolicy,
|
||||||
preparation_cache: &mut PreparationCache,
|
preparation_cache: &mut PreparationCache,
|
||||||
on_phase: &mut dyn FnMut(AssetPreparationPhase),
|
|
||||||
) -> Result<PreparedVisualBundle, AssetError> {
|
) -> Result<PreparedVisualBundle, AssetError> {
|
||||||
let PrototypeGeometry::Mesh(mesh_key) = &proto.geometry else {
|
let PrototypeGeometry::Mesh(mesh_key) = &proto.geometry else {
|
||||||
return Ok(PreparedVisualBundle {
|
return Ok(PreparedVisualBundle {
|
||||||
@@ -1779,7 +1531,6 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
on_phase(AssetPreparationPhase::ModelMesh);
|
|
||||||
let model = prepare_model_cached(repository, mesh_key, preparation_cache)?;
|
let model = prepare_model_cached(repository, mesh_key, preparation_cache)?;
|
||||||
let model_id = AssetId::new((identity_policy.model)(proto));
|
let model_id = AssetId::new((identity_policy.model)(proto));
|
||||||
let prepared_model = PreparedModel {
|
let prepared_model = PreparedModel {
|
||||||
@@ -1795,7 +1546,6 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
|
|||||||
name: wear_name,
|
name: wear_name,
|
||||||
type_id: Some(WEAR_KIND),
|
type_id: Some(WEAR_KIND),
|
||||||
};
|
};
|
||||||
on_phase(AssetPreparationPhase::WearTable);
|
|
||||||
let wear = prepare_wear_cached(repository, &wear_key, preparation_cache)?;
|
let wear = prepare_wear_cached(repository, &wear_key, preparation_cache)?;
|
||||||
let wear_id = AssetId::new((identity_policy.wear)(proto));
|
let wear_id = AssetId::new((identity_policy.wear)(proto));
|
||||||
let prepared_wear = PreparedWear {
|
let prepared_wear = PreparedWear {
|
||||||
@@ -1817,7 +1567,6 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|lightmap| lightmap.lightmap.clone())
|
.map(|lightmap| lightmap.lightmap.clone())
|
||||||
.collect();
|
.collect();
|
||||||
on_phase(AssetPreparationPhase::Materials);
|
|
||||||
for material_index in 0..wear.entries.len() {
|
for material_index in 0..wear.entries.len() {
|
||||||
let material_index = u16::try_from(material_index).map_err(|_| {
|
let material_index = u16::try_from(material_index).map_err(|_| {
|
||||||
AssetError::InvalidPrototype("material index does not fit archive format".to_string())
|
AssetError::InvalidPrototype("material index does not fit archive format".to_string())
|
||||||
@@ -1847,7 +1596,6 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
for texture in texture_requests {
|
for texture in texture_requests {
|
||||||
on_phase(AssetPreparationPhase::Textures);
|
|
||||||
let prepared_texture = prepare_texture_cached(
|
let prepared_texture = prepare_texture_cached(
|
||||||
repository,
|
repository,
|
||||||
&texture,
|
&texture,
|
||||||
@@ -1862,7 +1610,6 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for lightmap in &wear.lightmaps {
|
for lightmap in &wear.lightmaps {
|
||||||
on_phase(AssetPreparationPhase::Textures);
|
|
||||||
let prepared_lightmap = prepare_texture_cached(
|
let prepared_lightmap = prepare_texture_cached(
|
||||||
repository,
|
repository,
|
||||||
&lightmap.lightmap,
|
&lightmap.lightmap,
|
||||||
@@ -2450,66 +2197,6 @@ mod tests {
|
|||||||
use fparkan_vfs::{DirectoryVfs, MemoryVfs, Vfs};
|
use fparkan_vfs::{DirectoryVfs, MemoryVfs, Vfs};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn visual_dependency_progress_is_throttled_at_fixed_request_intervals() {
|
|
||||||
let mut progress = Vec::new();
|
|
||||||
for request_count in 1..=129 {
|
|
||||||
report_visual_dependency_progress(
|
|
||||||
&mut |event| progress.push(event),
|
|
||||||
VisualDependencyProgress {
|
|
||||||
phase: VisualDependencyPhase::Texture,
|
|
||||||
request_count,
|
|
||||||
graph_node_count: 37,
|
|
||||||
graph_edge_count: 41,
|
|
||||||
cache_entries: VisualDependencyCacheCounts {
|
|
||||||
wear_entries: 3,
|
|
||||||
material_entries: 5,
|
|
||||||
texture_entries: 7,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
progress,
|
|
||||||
vec![
|
|
||||||
VisualDependencyProgress {
|
|
||||||
phase: VisualDependencyPhase::Texture,
|
|
||||||
request_count: 1,
|
|
||||||
graph_node_count: 37,
|
|
||||||
graph_edge_count: 41,
|
|
||||||
cache_entries: VisualDependencyCacheCounts {
|
|
||||||
wear_entries: 3,
|
|
||||||
material_entries: 5,
|
|
||||||
texture_entries: 7,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
VisualDependencyProgress {
|
|
||||||
phase: VisualDependencyPhase::Texture,
|
|
||||||
request_count: 64,
|
|
||||||
graph_node_count: 37,
|
|
||||||
graph_edge_count: 41,
|
|
||||||
cache_entries: VisualDependencyCacheCounts {
|
|
||||||
wear_entries: 3,
|
|
||||||
material_entries: 5,
|
|
||||||
texture_entries: 7,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
VisualDependencyProgress {
|
|
||||||
phase: VisualDependencyPhase::Texture,
|
|
||||||
request_count: 128,
|
|
||||||
graph_node_count: 37,
|
|
||||||
graph_edge_count: 41,
|
|
||||||
cache_entries: VisualDependencyCacheCounts {
|
|
||||||
wear_entries: 3,
|
|
||||||
material_entries: 5,
|
|
||||||
texture_entries: 7,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wear_validation_cache_replays_archive_qualified_failure() {
|
fn wear_validation_cache_replays_archive_qualified_failure() {
|
||||||
let repository = repository_with_archives(&[]);
|
let repository = repository_with_archives(&[]);
|
||||||
@@ -3315,7 +3002,6 @@ mod tests {
|
|||||||
&prototypes,
|
&prototypes,
|
||||||
policy,
|
policy,
|
||||||
AssetPreparationLimits::default(),
|
AssetPreparationLimits::default(),
|
||||||
&mut |_| {},
|
|
||||||
)
|
)
|
||||||
.expect_err("collision should fail");
|
.expect_err("collision should fail");
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,3 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Bounded little-endian binary cursor and checked layout helpers.
|
//! Bounded little-endian binary cursor and checked layout helpers.
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@@ -399,7 +380,6 @@ pub fn sha256_hex(digest: &Sha256Digest) -> String {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::many_single_char_names)]
|
|
||||||
fn compress_sha256_chunk(h: &mut [u32; 8], chunk: &[u8], k: &[u32; 64]) {
|
fn compress_sha256_chunk(h: &mut [u32; 8], chunk: &[u8], k: &[u32; 64]) {
|
||||||
let mut w = [0u32; 64];
|
let mut w = [0u32; 64];
|
||||||
for (idx, word) in w.iter_mut().take(16).enumerate() {
|
for (idx, word) in w.iter_mut().take(16).enumerate() {
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "fparkan-corpus"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
repository.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
|
|
||||||
fparkan-fx = { path = "../fparkan-fx", version = "0.1.0" }
|
|
||||||
fparkan-material = { path = "../fparkan-material", version = "0.1.0" }
|
|
||||||
fparkan-msh = { path = "../fparkan-msh", version = "0.1.0" }
|
|
||||||
fparkan-mission-format = { path = "../fparkan-mission-format", version = "0.1.0" }
|
|
||||||
fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
|
|
||||||
fparkan-prototype = { path = "../fparkan-prototype", version = "0.1.0" }
|
|
||||||
fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
|
|
||||||
fparkan-rsli = { path = "../fparkan-rsli", version = "0.1.0" }
|
|
||||||
fparkan-texm = { path = "../fparkan-texm", version = "0.1.0" }
|
|
||||||
fparkan-terrain-format = { path = "../fparkan-terrain-format", version = "0.1.0" }
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "fparkan-diagnostics"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
repository.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
#![forbid(unsafe_code)]
|
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Structured diagnostics shared by `FParkan` crates.
|
|
||||||
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
/// Diagnostic severity.
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum Severity {
|
|
||||||
/// Informational note.
|
|
||||||
Info,
|
|
||||||
/// Recoverable warning.
|
|
||||||
Warning,
|
|
||||||
/// Error for the current operation.
|
|
||||||
Error,
|
|
||||||
/// Fatal error for the current run.
|
|
||||||
Fatal,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Evidence level for a contract or interpretation.
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum EvidenceStatus {
|
|
||||||
/// Described by project documentation.
|
|
||||||
Documented,
|
|
||||||
/// Verified by synthetic fixtures.
|
|
||||||
SyntheticVerified,
|
|
||||||
/// Verified against the licensed corpus.
|
|
||||||
CorpusVerified,
|
|
||||||
/// Verified by runtime capture.
|
|
||||||
RuntimeCaptured,
|
|
||||||
/// Working hypothesis; not a runtime contract.
|
|
||||||
Hypothesis,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Operation phase where a diagnostic was produced.
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum Phase {
|
|
||||||
/// Discovery.
|
|
||||||
Discover,
|
|
||||||
/// Read.
|
|
||||||
Read,
|
|
||||||
/// Parse.
|
|
||||||
Parse,
|
|
||||||
/// Validate.
|
|
||||||
Validate,
|
|
||||||
/// Resolve.
|
|
||||||
Resolve,
|
|
||||||
/// Prepare.
|
|
||||||
Prepare,
|
|
||||||
/// Construct.
|
|
||||||
Construct,
|
|
||||||
/// Register.
|
|
||||||
Register,
|
|
||||||
/// Simulate.
|
|
||||||
Simulate,
|
|
||||||
/// Render.
|
|
||||||
Render,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Byte span in an input source.
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
|
|
||||||
pub struct SourceSpan {
|
|
||||||
/// Start offset.
|
|
||||||
pub offset: u64,
|
|
||||||
/// Length in bytes.
|
|
||||||
pub length: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stable diagnostic code.
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize)]
|
|
||||||
pub struct DiagnosticCode(pub &'static str);
|
|
||||||
|
|
||||||
/// Context attached to a diagnostic.
|
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
|
|
||||||
pub struct DiagnosticContext {
|
|
||||||
/// Phase.
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub phase: Option<Phase>,
|
|
||||||
/// Redacted or logical path.
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub path: Option<String>,
|
|
||||||
/// Archive entry name.
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub archive_entry: Option<String>,
|
|
||||||
/// Object/prototype key.
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub object_key: Option<String>,
|
|
||||||
/// Input span.
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub span: Option<SourceSpan>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Structured diagnostic with cause chain.
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
|
||||||
pub struct Diagnostic {
|
|
||||||
/// Stable code.
|
|
||||||
pub code: DiagnosticCode,
|
|
||||||
/// Severity.
|
|
||||||
pub severity: Severity,
|
|
||||||
/// Human message.
|
|
||||||
pub message: String,
|
|
||||||
/// Context.
|
|
||||||
pub context: DiagnosticContext,
|
|
||||||
/// Causes.
|
|
||||||
pub causes: Vec<Diagnostic>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a diagnostic with default error severity.
|
|
||||||
#[must_use]
|
|
||||||
pub fn diagnostic(code: DiagnosticCode, message: impl Into<String>) -> Diagnostic {
|
|
||||||
Diagnostic {
|
|
||||||
code,
|
|
||||||
severity: Severity::Error,
|
|
||||||
message: message.into(),
|
|
||||||
context: DiagnosticContext::default(),
|
|
||||||
causes: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Diagnostic {
|
|
||||||
/// Returns a copy with severity changed.
|
|
||||||
#[must_use]
|
|
||||||
pub fn with_severity(mut self, severity: Severity) -> Self {
|
|
||||||
self.severity = severity;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a copy with context changed.
|
|
||||||
#[must_use]
|
|
||||||
pub fn with_context(mut self, context: DiagnosticContext) -> Self {
|
|
||||||
self.context = context;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a cause.
|
|
||||||
pub fn push_cause(&mut self, cause: Diagnostic) {
|
|
||||||
self.causes.push(cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders a compact human-readable diagnostic.
|
|
||||||
#[must_use]
|
|
||||||
pub fn render_human(diagnostic: &Diagnostic) -> String {
|
|
||||||
let mut out = format!(
|
|
||||||
"{:?} {}: {}",
|
|
||||||
diagnostic.severity, diagnostic.code.0, diagnostic.message
|
|
||||||
);
|
|
||||||
if let Some(path) = &diagnostic.context.path {
|
|
||||||
out.push_str(" [");
|
|
||||||
out.push_str(path);
|
|
||||||
out.push(']');
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders deterministic JSON using the typed diagnostic schema.
|
|
||||||
#[must_use]
|
|
||||||
pub fn render_json(diagnostic: &Diagnostic) -> String {
|
|
||||||
match serde_json::to_string(diagnostic) {
|
|
||||||
Ok(json) => json,
|
|
||||||
Err(err) => format!("{{\"error\":\"diagnostic serialization failed: {err}\"}}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn json_is_stable() {
|
|
||||||
let d = diagnostic(DiagnosticCode("S0-DIAG-001"), "keeps context").with_context(
|
|
||||||
DiagnosticContext {
|
|
||||||
phase: Some(Phase::Parse),
|
|
||||||
..DiagnosticContext::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
render_json(&d),
|
|
||||||
"{\"code\":\"S0-DIAG-001\",\"severity\":\"error\",\"message\":\"keeps context\",\"context\":{\"phase\":\"parse\"},\"causes\":[]}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn diagnostic_chain_preserves_context() {
|
|
||||||
let mut root = diagnostic(DiagnosticCode("ROOT"), "root").with_context(DiagnosticContext {
|
|
||||||
phase: Some(Phase::Resolve),
|
|
||||||
path: Some("archives/material.lib".to_string()),
|
|
||||||
archive_entry: Some("MATERIAL.MAT0".to_string()),
|
|
||||||
object_key: Some("unit/tank".to_string()),
|
|
||||||
span: Some(SourceSpan {
|
|
||||||
offset: 12,
|
|
||||||
length: 4,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
root.push_cause(diagnostic(DiagnosticCode("CAUSE"), "cause").with_context(
|
|
||||||
DiagnosticContext {
|
|
||||||
phase: Some(Phase::Parse),
|
|
||||||
path: Some("archives/material.lib".to_string()),
|
|
||||||
span: Some(SourceSpan {
|
|
||||||
offset: 16,
|
|
||||||
length: 8,
|
|
||||||
}),
|
|
||||||
..DiagnosticContext::default()
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
let json = render_json(&root);
|
|
||||||
|
|
||||||
assert!(json.contains("\"code\":\"ROOT\""));
|
|
||||||
assert!(json.contains("\"phase\":\"resolve\""));
|
|
||||||
assert!(json.contains("\"path\":\"archives/material.lib\""));
|
|
||||||
assert!(json.contains("\"archive_entry\":\"MATERIAL.MAT0\""));
|
|
||||||
assert!(json.contains("\"object_key\":\"unit/tank\""));
|
|
||||||
assert!(json.contains("\"span\":{\"offset\":12,\"length\":4}"));
|
|
||||||
assert!(json.contains("\"code\":\"CAUSE\""));
|
|
||||||
assert!(json.contains("\"span\":{\"offset\":16,\"length\":8}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn json_escapes_all_control_characters() {
|
|
||||||
let value = diagnostic(DiagnosticCode("S1-H01"), "quote\"\u{0000}tab\tline\r\n");
|
|
||||||
let json = render_json(&value);
|
|
||||||
assert!(json.contains("\\u0000"));
|
|
||||||
assert!(json.contains("\\t"));
|
|
||||||
assert!(!json.contains('\t'));
|
|
||||||
assert!(!json.contains('\r'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,10 +6,7 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
|
fparkan-binary = { path = "../fparkan-binary" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
|
fparkan-nres = { path = "../fparkan-nres" }
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! FXID effect contracts.
|
//! FXID effect contracts.
|
||||||
//!
|
//!
|
||||||
//! FXID decoding and command framing are implemented as compatibility
|
//! FXID decoding and command framing are implemented as compatibility
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "fparkan-inspection"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
repository.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
fparkan-diagnostics = { path = "../fparkan-diagnostics", version = "0.1.0" }
|
|
||||||
fparkan-msh = { path = "../fparkan-msh", version = "0.1.0" }
|
|
||||||
fparkan-material = { path = "../fparkan-material", version = "0.1.0" }
|
|
||||||
fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
|
|
||||||
fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
|
|
||||||
fparkan-rsli = { path = "../fparkan-rsli", version = "0.1.0" }
|
|
||||||
fparkan-resource = { path = "../fparkan-resource", version = "0.1.0" }
|
|
||||||
fparkan-terrain-format = { path = "../fparkan-terrain-format", version = "0.1.0" }
|
|
||||||
fparkan-texm = { path = "../fparkan-texm", version = "0.1.0" }
|
|
||||||
fparkan-vfs = { path = "../fparkan-vfs", version = "0.1.0" }
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -7,12 +7,9 @@ repository.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
|
fparkan-path = { path = "../fparkan-path" }
|
||||||
fparkan-resource = { path = "../fparkan-resource", version = "0.1.0" }
|
fparkan-resource = { path = "../fparkan-resource" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
|
fparkan-nres = { path = "../fparkan-nres" }
|
||||||
fparkan-vfs = { path = "../fparkan-vfs", version = "0.1.0" }
|
fparkan-vfs = { path = "../fparkan-vfs" }
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! WEAR/MAT0 material contracts.
|
//! WEAR/MAT0 material contracts.
|
||||||
|
|
||||||
use encoding_rs::WINDOWS_1251;
|
use encoding_rs::WINDOWS_1251;
|
||||||
|
|||||||
@@ -7,7 +7,4 @@ repository.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
|
fparkan-binary = { path = "../fparkan-binary" }
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Count-driven mission format primitives.
|
//! Count-driven mission format primitives.
|
||||||
|
|
||||||
use encoding_rs::WINDOWS_1251;
|
use encoding_rs::WINDOWS_1251;
|
||||||
|
|||||||
@@ -7,8 +7,5 @@ repository.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
fparkan-animation = { path = "../fparkan-animation", version = "0.1.0" }
|
fparkan-animation = { path = "../fparkan-animation" }
|
||||||
fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
|
fparkan-nres = { path = "../fparkan-nres" }
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Stage-3 MSH asset contract.
|
//! Stage-3 MSH asset contract.
|
||||||
|
|
||||||
use encoding_rs::WINDOWS_1251;
|
use encoding_rs::WINDOWS_1251;
|
||||||
@@ -2065,7 +2046,6 @@ mod tests {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::similar_names, clippy::too_many_arguments)]
|
|
||||||
fn batch_record(
|
fn batch_record(
|
||||||
batch_flags: u16,
|
batch_flags: u16,
|
||||||
material_index: u16,
|
material_index: u16,
|
||||||
|
|||||||
@@ -6,8 +6,5 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
|
fparkan-binary = { path = "../fparkan-binary" }
|
||||||
fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
|
fparkan-path = { path = "../fparkan-path" }
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Strict and lossless `NRes` archive support.
|
//! Strict and lossless `NRes` archive support.
|
||||||
|
|
||||||
use fparkan_binary::{checked_allocation_len, Cursor, DecodeError};
|
use fparkan_binary::{checked_allocation_len, Cursor, DecodeError};
|
||||||
|
|||||||
@@ -6,6 +6,3 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Legacy path normalization and ASCII lookup semantics.
|
//! Legacy path normalization and ASCII lookup semantics.
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|||||||
@@ -7,6 +7,3 @@ repository.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
raw-window-handle = "0.6"
|
raw-window-handle = "0.6"
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Platform ports for clocks, event sources and window descriptors.
|
//! Platform ports for clocks, event sources and window descriptors.
|
||||||
|
|
||||||
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
|
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
|
||||||
|
|||||||
@@ -7,13 +7,10 @@ repository.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
|
fparkan-binary = { path = "../fparkan-binary" }
|
||||||
fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
|
fparkan-path = { path = "../fparkan-path" }
|
||||||
fparkan-resource = { path = "../fparkan-resource", version = "0.1.0" }
|
fparkan-resource = { path = "../fparkan-resource" }
|
||||||
fparkan-vfs = { path = "../fparkan-vfs", version = "0.1.0" }
|
fparkan-vfs = { path = "../fparkan-vfs" }
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
|
fparkan-nres = { path = "../fparkan-nres" }
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Prototype registry and unit DAT primitives.
|
//! Prototype registry and unit DAT primitives.
|
||||||
|
|
||||||
use encoding_rs::WINDOWS_1251;
|
use encoding_rs::WINDOWS_1251;
|
||||||
@@ -824,7 +805,7 @@ pub fn build_prototype_graph(
|
|||||||
///
|
///
|
||||||
/// This function reports per-root failures in [`PrototypeGraphReport`] instead
|
/// This function reports per-root failures in [`PrototypeGraphReport`] instead
|
||||||
/// of returning early.
|
/// of returning early.
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
pub fn build_prototype_graph_report(
|
pub fn build_prototype_graph_report(
|
||||||
repository: &dyn ResourceRepository,
|
repository: &dyn ResourceRepository,
|
||||||
vfs: &dyn Vfs,
|
vfs: &dyn Vfs,
|
||||||
|
|||||||
@@ -6,7 +6,3 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fparkan-world = { path = "../fparkan-world", version = "0.1.0" }
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,11 +6,8 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
|
fparkan-binary = { path = "../fparkan-binary" }
|
||||||
fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
|
fparkan-nres = { path = "../fparkan-nres" }
|
||||||
fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
|
fparkan-path = { path = "../fparkan-path" }
|
||||||
fparkan-rsli = { path = "../fparkan-rsli", version = "0.1.0" }
|
fparkan-rsli = { path = "../fparkan-rsli" }
|
||||||
fparkan-vfs = { path = "../fparkan-vfs", version = "0.1.0" }
|
fparkan-vfs = { path = "../fparkan-vfs" }
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Resource identity and repository ports.
|
//! Resource identity and repository ports.
|
||||||
|
|
||||||
use fparkan_binary::{sha256, Sha256Digest};
|
use fparkan_binary::{sha256, Sha256Digest};
|
||||||
|
|||||||
@@ -6,8 +6,5 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
|
fparkan-binary = { path = "../fparkan-binary" }
|
||||||
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Stage-1 `RsLi` archive contract.
|
//! Stage-1 `RsLi` archive contract.
|
||||||
|
|
||||||
use fparkan_binary::DecodeError;
|
use fparkan_binary::DecodeError;
|
||||||
@@ -954,7 +935,6 @@ struct EntryRecord {
|
|||||||
effective_offset: usize,
|
effective_offset: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
fn parse_rsli(bytes: &[u8], options: ParseOptions) -> Result<ParsedRsli, RsliError> {
|
fn parse_rsli(bytes: &[u8], options: ParseOptions) -> Result<ParsedRsli, RsliError> {
|
||||||
enforce_limit(
|
enforce_limit(
|
||||||
u64::try_from(bytes.len()).map_err(|_| RsliError::IntegerOverflow)?,
|
u64::try_from(bytes.len()).map_err(|_| RsliError::IntegerOverflow)?,
|
||||||
|
|||||||
@@ -6,16 +6,13 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fparkan-assets = { path = "../fparkan-assets", version = "0.1.0" }
|
fparkan-assets = { path = "../fparkan-assets" }
|
||||||
fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
|
fparkan-path = { path = "../fparkan-path" }
|
||||||
fparkan-platform = { path = "../fparkan-platform", version = "0.1.0" }
|
fparkan-platform = { path = "../fparkan-platform" }
|
||||||
fparkan-prototype = { path = "../fparkan-prototype", version = "0.1.0" }
|
fparkan-prototype = { path = "../fparkan-prototype" }
|
||||||
fparkan-render = { path = "../fparkan-render", version = "0.1.0" }
|
fparkan-render = { path = "../fparkan-render" }
|
||||||
fparkan-terrain = { path = "../fparkan-terrain", version = "0.1.0" }
|
fparkan-terrain = { path = "../fparkan-terrain" }
|
||||||
fparkan-resource = { path = "../fparkan-resource", version = "0.1.0" }
|
fparkan-resource = { path = "../fparkan-resource" }
|
||||||
fparkan-script = { path = "../fparkan-script", version = "0.1.0" }
|
fparkan-script = { path = "../fparkan-script" }
|
||||||
fparkan-vfs = { path = "../fparkan-vfs", version = "0.1.0" }
|
fparkan-vfs = { path = "../fparkan-vfs" }
|
||||||
fparkan-world = { path = "../fparkan-world", version = "0.1.0" }
|
fparkan-world = { path = "../fparkan-world" }
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,32 +1,13 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
//! Runtime orchestration for mission loading and world stepping.
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Runtime orchestration for headless and rendered modes.
|
|
||||||
|
|
||||||
use fparkan_assets::{
|
use fparkan_assets::{
|
||||||
decode_mission_land_path, decode_mission_payload, decode_nres_payload,
|
decode_mission_land_path, decode_mission_payload, decode_nres_payload,
|
||||||
derive_mission_land_paths, extend_graph_report_with_visual_dependencies_with_progress,
|
derive_mission_land_paths, extend_graph_report_with_visual_dependencies,
|
||||||
mission_script_bundle_bases, prepare_terrain_world, AssetError as AssetPreparationError,
|
mission_script_bundle_bases, prepare_terrain_world, AssetError as AssetPreparationError,
|
||||||
AssetId, AssetManager, AssetPreparationPhase, BuildCategory, MissionAssetPlan, MissionDocument,
|
AssetId, AssetManager, BuildCategory, MissionAssetPlan, MissionDocument, MissionError,
|
||||||
MissionError, MissionTerrainPaths, NresError, PreparedVisual, TerrainFormatError,
|
MissionTerrainPaths, NresError, PreparedVisual, TerrainFormatError, TerrainPreparationError,
|
||||||
TerrainPreparationError, TerrainWorld, TmaProfile, VisualDependencyPhase,
|
TerrainWorld, TmaProfile,
|
||||||
};
|
};
|
||||||
use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy};
|
use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy};
|
||||||
use fparkan_prototype::{
|
use fparkan_prototype::{
|
||||||
@@ -53,47 +34,6 @@ pub use fparkan_assets::MissionAssets;
|
|||||||
const MISSION_DECODED_PAYLOAD_CACHE_ENTRIES: usize = 256;
|
const MISSION_DECODED_PAYLOAD_CACHE_ENTRIES: usize = 256;
|
||||||
const FALLBACK_SCRIPT_VARSET_PATH: &str = "MISSIONS/SCRIPTS/varset.var";
|
const FALLBACK_SCRIPT_VARSET_PATH: &str = "MISSIONS/SCRIPTS/varset.var";
|
||||||
|
|
||||||
/// Engine mode.
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
pub enum EngineMode {
|
|
||||||
/// Headless.
|
|
||||||
Headless,
|
|
||||||
/// Rendered.
|
|
||||||
Rendered,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scheduler phase.
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
pub enum SchedulerPhase {
|
|
||||||
/// Collect platform events.
|
|
||||||
CollectPlatformEvents,
|
|
||||||
/// Build input snapshot.
|
|
||||||
BuildInputSnapshot,
|
|
||||||
/// Advance clock.
|
|
||||||
AdvanceGameClock,
|
|
||||||
/// Calculate world queue.
|
|
||||||
CalculateWorldQueue,
|
|
||||||
/// Apply deferred operations.
|
|
||||||
ApplyDeferredOperations,
|
|
||||||
/// Update animation/effects.
|
|
||||||
UpdateAnimationAndEffects,
|
|
||||||
/// Publish render snapshot.
|
|
||||||
PublishRenderSnapshot,
|
|
||||||
/// Render world.
|
|
||||||
RenderWorld,
|
|
||||||
/// End frame callbacks.
|
|
||||||
EndFrameCallbacks,
|
|
||||||
/// Maintenance.
|
|
||||||
Maintenance,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Engine config.
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
pub struct EngineConfig {
|
|
||||||
/// Mode.
|
|
||||||
pub mode: EngineMode,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Injectable engine services used by composition roots.
|
/// Injectable engine services used by composition roots.
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct EngineServices {
|
pub struct EngineServices {
|
||||||
@@ -116,97 +56,6 @@ pub struct MissionRequest {
|
|||||||
pub key: String,
|
pub key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mission loading phase captured for diagnostics and acceptance tests.
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
pub enum MissionLoadPhase {
|
|
||||||
/// Resolve services and mission request context.
|
|
||||||
Context,
|
|
||||||
/// Decode and validate TMA.
|
|
||||||
Tma,
|
|
||||||
/// Decode and validate terrain map assets.
|
|
||||||
Map,
|
|
||||||
/// Expand object roots into a prototype graph.
|
|
||||||
Graph,
|
|
||||||
/// Expand model-backed visual dependencies of the prototype graph.
|
|
||||||
GraphVisuals,
|
|
||||||
/// Resolve WEAR tables while expanding visual dependencies.
|
|
||||||
GraphVisualWears,
|
|
||||||
/// Resolve MAT0 documents while expanding visual dependencies.
|
|
||||||
GraphVisualMaterials,
|
|
||||||
/// Progress marker emitted after each 64 MAT0 validation requests.
|
|
||||||
GraphVisualMaterialRequests {
|
|
||||||
/// Number of MAT0 validation requests started.
|
|
||||||
request_count: usize,
|
|
||||||
/// Graph nodes materialized before the request.
|
|
||||||
graph_node_count: usize,
|
|
||||||
/// Graph edges materialized before the request.
|
|
||||||
graph_edge_count: usize,
|
|
||||||
/// Distinct WEAR validation keys retained before the request.
|
|
||||||
wear_cache_entries: usize,
|
|
||||||
/// Distinct MAT0 validation keys retained before the request.
|
|
||||||
material_cache_entries: usize,
|
|
||||||
/// Distinct TEXM validation keys retained before the request.
|
|
||||||
texture_cache_entries: usize,
|
|
||||||
},
|
|
||||||
/// Validate TEXM documents while expanding visual dependencies.
|
|
||||||
GraphVisualTextures,
|
|
||||||
/// Progress marker emitted after each 64 TEXM validation requests.
|
|
||||||
GraphVisualTextureRequests {
|
|
||||||
/// Number of TEXM validation requests started.
|
|
||||||
request_count: usize,
|
|
||||||
/// Graph nodes materialized before the request.
|
|
||||||
graph_node_count: usize,
|
|
||||||
/// Graph edges materialized before the request.
|
|
||||||
graph_edge_count: usize,
|
|
||||||
/// Distinct WEAR validation keys retained before the request.
|
|
||||||
wear_cache_entries: usize,
|
|
||||||
/// Distinct MAT0 validation keys retained before the request.
|
|
||||||
material_cache_entries: usize,
|
|
||||||
/// Distinct TEXM validation keys retained before the request.
|
|
||||||
texture_cache_entries: usize,
|
|
||||||
},
|
|
||||||
/// Prepare all reachable visual/resource dependencies.
|
|
||||||
Assets,
|
|
||||||
/// Decode and validate MSH model meshes.
|
|
||||||
AssetModelMeshes,
|
|
||||||
/// Decode WEAR material tables.
|
|
||||||
AssetWearTables,
|
|
||||||
/// Resolve MAT0 material documents.
|
|
||||||
AssetMaterials,
|
|
||||||
/// Decode diffuse textures and baked lightmaps.
|
|
||||||
AssetTextures,
|
|
||||||
/// Construct all object drafts before registration.
|
|
||||||
Construct,
|
|
||||||
/// Register constructed objects.
|
|
||||||
Register,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Raw placed transform preserved by the mission loader.
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct PlacedTransformProfile {
|
|
||||||
/// Object index in TMA order.
|
|
||||||
pub object_index: usize,
|
|
||||||
/// Raw position vector.
|
|
||||||
pub position: [f32; 3],
|
|
||||||
/// Raw orientation vector. No Euler order is inferred here.
|
|
||||||
pub orientation_raw: [f32; 3],
|
|
||||||
/// Raw scale vector.
|
|
||||||
pub scale: [f32; 3],
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mission loading trace.
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq)]
|
|
||||||
pub struct MissionLoadTrace {
|
|
||||||
/// Observed phases in execution order.
|
|
||||||
pub phases: Vec<MissionLoadPhase>,
|
|
||||||
/// Number of object drafts constructed before the first registration.
|
|
||||||
pub drafts_before_registration: usize,
|
|
||||||
/// Number of objects registered.
|
|
||||||
pub registered_objects: usize,
|
|
||||||
/// Raw transform profile for placed objects.
|
|
||||||
pub transforms: Vec<PlacedTransformProfile>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ordered mission property preserved for later runtime stages.
|
/// Ordered mission property preserved for later runtime stages.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct MissionObjectProperty {
|
pub struct MissionObjectProperty {
|
||||||
@@ -435,20 +284,10 @@ pub struct LoadedMission {
|
|||||||
pub struct FrameResult {
|
pub struct FrameResult {
|
||||||
/// Snapshot.
|
/// Snapshot.
|
||||||
pub snapshot: WorldSnapshot,
|
pub snapshot: WorldSnapshot,
|
||||||
/// Scheduler phases executed for this frame.
|
|
||||||
pub trace: FrameTrace,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scheduler trace for a completed frame.
|
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
|
||||||
pub struct FrameTrace {
|
|
||||||
/// Frame phases in execution order.
|
|
||||||
pub phases: Vec<SchedulerPhase>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Engine.
|
/// Engine.
|
||||||
pub struct Engine {
|
pub struct Engine {
|
||||||
config: EngineConfig,
|
|
||||||
services: EngineServices,
|
services: EngineServices,
|
||||||
world: World,
|
world: World,
|
||||||
loaded: Option<LoadedMissionState>,
|
loaded: Option<LoadedMissionState>,
|
||||||
@@ -559,13 +398,6 @@ pub enum EngineError {
|
|||||||
World(fparkan_world::WorldError),
|
World(fparkan_world::WorldError),
|
||||||
/// Reference movement input or terrain query failed.
|
/// Reference movement input or terrain query failed.
|
||||||
Movement(String),
|
Movement(String),
|
||||||
/// Scheduler phase order was violated.
|
|
||||||
SchedulerPhaseOrder {
|
|
||||||
/// Previous phase.
|
|
||||||
previous: SchedulerPhase,
|
|
||||||
/// Current phase.
|
|
||||||
current: SchedulerPhase,
|
|
||||||
},
|
|
||||||
/// Staged mission world was torn down after a registration-phase failure.
|
/// Staged mission world was torn down after a registration-phase failure.
|
||||||
RegistrationTeardown {
|
RegistrationTeardown {
|
||||||
/// Registered objects before the forced failure.
|
/// Registered objects before the forced failure.
|
||||||
@@ -676,10 +508,6 @@ impl std::fmt::Display for EngineError {
|
|||||||
}
|
}
|
||||||
Self::World(source) => write!(f, "{source}"),
|
Self::World(source) => write!(f, "{source}"),
|
||||||
Self::Movement(message) => write!(f, "reference movement: {message}"),
|
Self::Movement(message) => write!(f, "reference movement: {message}"),
|
||||||
Self::SchedulerPhaseOrder { previous, current } => write!(
|
|
||||||
f,
|
|
||||||
"scheduler phase order regressed from {previous:?} to {current:?}"
|
|
||||||
),
|
|
||||||
Self::RegistrationTeardown {
|
Self::RegistrationTeardown {
|
||||||
registered_objects,
|
registered_objects,
|
||||||
released_objects,
|
released_objects,
|
||||||
@@ -710,22 +538,14 @@ impl std::error::Error for EngineError {
|
|||||||
| Self::ScriptRuntime { .. }
|
| Self::ScriptRuntime { .. }
|
||||||
| Self::Movement(_)
|
| Self::Movement(_)
|
||||||
| Self::PrototypeGraph { .. }
|
| Self::PrototypeGraph { .. }
|
||||||
| Self::SchedulerPhaseOrder { .. }
|
|
||||||
| Self::RegistrationTeardown { .. } => None,
|
| Self::RegistrationTeardown { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates engine.
|
/// Creates an engine with the supplied services.
|
||||||
///
|
pub fn create(services: EngineServices) -> Result<Engine, EngineError> {
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Currently this constructor is infallible, but it returns
|
|
||||||
/// [`EngineError`] to keep the composition-root API stable as services become
|
|
||||||
/// mandatory.
|
|
||||||
pub fn create(config: EngineConfig, services: EngineServices) -> Result<Engine, EngineError> {
|
|
||||||
Ok(Engine {
|
Ok(Engine {
|
||||||
config,
|
|
||||||
services,
|
services,
|
||||||
world: new_world(WorldConfig),
|
world: new_world(WorldConfig),
|
||||||
loaded: None,
|
loaded: None,
|
||||||
@@ -744,7 +564,7 @@ pub fn load_mission(
|
|||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
request: MissionRequest,
|
request: MissionRequest,
|
||||||
) -> Result<LoadedMission, EngineError> {
|
) -> Result<LoadedMission, EngineError> {
|
||||||
load_mission_with_trace(engine, request).map(|(loaded, _trace)| loaded)
|
load_mission_with_options(engine, request, MissionLoadOptions::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads a mission using the bounded static-preview asset scope.
|
/// Loads a mission using the bounded static-preview asset scope.
|
||||||
@@ -763,7 +583,14 @@ pub fn load_mission_static_preview(
|
|||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
request: MissionRequest,
|
request: MissionRequest,
|
||||||
) -> Result<LoadedMission, EngineError> {
|
) -> Result<LoadedMission, EngineError> {
|
||||||
load_mission_static_preview_with_progress(engine, request, |_| {})
|
load_mission_with_options(
|
||||||
|
engine,
|
||||||
|
request,
|
||||||
|
MissionLoadOptions {
|
||||||
|
asset_scope: MissionAssetScope::FirstMeshPreview,
|
||||||
|
..MissionLoadOptions::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads a static preview for the first requested non-zero count of mission roots.
|
/// Loads a static preview for the first requested non-zero count of mission roots.
|
||||||
@@ -779,107 +606,25 @@ pub fn load_mission_static_preview_roots(
|
|||||||
request: MissionRequest,
|
request: MissionRequest,
|
||||||
root_count: NonZeroUsize,
|
root_count: NonZeroUsize,
|
||||||
) -> Result<LoadedMission, EngineError> {
|
) -> Result<LoadedMission, EngineError> {
|
||||||
load_mission_static_preview_roots_with_progress(engine, request, root_count, |_| {})
|
load_mission_with_options(
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads selected static-preview roots while synchronously reporting phases.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns [`EngineError`] under the same conditions as
|
|
||||||
/// [`load_mission_static_preview_roots`].
|
|
||||||
pub fn load_mission_static_preview_roots_with_progress(
|
|
||||||
engine: &mut Engine,
|
|
||||||
request: MissionRequest,
|
|
||||||
root_count: NonZeroUsize,
|
|
||||||
mut on_phase: impl FnMut(MissionLoadPhase),
|
|
||||||
) -> Result<LoadedMission, EngineError> {
|
|
||||||
load_mission_with_options_and_progress(
|
|
||||||
engine,
|
engine,
|
||||||
request,
|
request,
|
||||||
MissionLoadOptions {
|
MissionLoadOptions {
|
||||||
asset_scope: MissionAssetScope::PreviewRoots(root_count),
|
asset_scope: MissionAssetScope::PreviewRoots(root_count),
|
||||||
..MissionLoadOptions::default()
|
..MissionLoadOptions::default()
|
||||||
},
|
},
|
||||||
Some(&mut on_phase),
|
|
||||||
)
|
)
|
||||||
.map(|(loaded, _trace)| loaded)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads a static preview while synchronously reporting entered loading phases.
|
fn load_mission_with_options(
|
||||||
///
|
|
||||||
/// This has the same bounded asset scope as [`load_mission_static_preview`].
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns [`EngineError`] under the same conditions as
|
|
||||||
/// [`load_mission_static_preview`].
|
|
||||||
pub fn load_mission_static_preview_with_progress(
|
|
||||||
engine: &mut Engine,
|
|
||||||
request: MissionRequest,
|
|
||||||
mut on_phase: impl FnMut(MissionLoadPhase),
|
|
||||||
) -> Result<LoadedMission, EngineError> {
|
|
||||||
load_mission_with_options_and_progress(
|
|
||||||
engine,
|
|
||||||
request,
|
|
||||||
MissionLoadOptions {
|
|
||||||
asset_scope: MissionAssetScope::FirstMeshPreview,
|
|
||||||
..MissionLoadOptions::default()
|
|
||||||
},
|
|
||||||
Some(&mut on_phase),
|
|
||||||
)
|
|
||||||
.map(|(loaded, _trace)| loaded)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads mission transactionally and returns a diagnostic trace.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns [`EngineError`] under the same conditions as [`load_mission`].
|
|
||||||
pub fn load_mission_with_trace(
|
|
||||||
engine: &mut Engine,
|
|
||||||
request: MissionRequest,
|
|
||||||
) -> Result<(LoadedMission, MissionLoadTrace), EngineError> {
|
|
||||||
load_mission_with_options_and_progress(engine, request, MissionLoadOptions::default(), None)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads a mission while synchronously reporting each entered loading phase.
|
|
||||||
///
|
|
||||||
/// The observer runs immediately after the phase is recorded in the returned
|
|
||||||
/// trace. It is intended for diagnostic progress reporting; it does not alter
|
|
||||||
/// loader ordering, validation, or transaction behavior.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns [`EngineError`] under the same conditions as [`load_mission`].
|
|
||||||
pub fn load_mission_with_progress(
|
|
||||||
engine: &mut Engine,
|
|
||||||
request: MissionRequest,
|
|
||||||
mut on_phase: impl FnMut(MissionLoadPhase),
|
|
||||||
) -> Result<LoadedMission, EngineError> {
|
|
||||||
load_mission_with_options_and_progress(
|
|
||||||
engine,
|
|
||||||
request,
|
|
||||||
MissionLoadOptions::default(),
|
|
||||||
Some(&mut on_phase),
|
|
||||||
)
|
|
||||||
.map(|(loaded, _trace)| loaded)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
fn load_mission_with_options_and_progress(
|
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
request: MissionRequest,
|
request: MissionRequest,
|
||||||
options: MissionLoadOptions,
|
options: MissionLoadOptions,
|
||||||
mut on_phase: Option<&mut dyn FnMut(MissionLoadPhase)>,
|
) -> Result<LoadedMission, EngineError> {
|
||||||
) -> Result<(LoadedMission, MissionLoadTrace), EngineError> {
|
|
||||||
let mut trace = MissionLoadTrace::default();
|
|
||||||
record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Context);
|
|
||||||
let vfs = engine.services.vfs.clone().ok_or(EngineError::MissingVfs)?;
|
let vfs = engine.services.vfs.clone().ok_or(EngineError::MissingVfs)?;
|
||||||
let mission_path = normalize_engine_path("mission", &request.key)?;
|
let mission_path = normalize_engine_path("mission", &request.key)?;
|
||||||
let mission_bytes = read_vfs(&vfs, &mission_path)?;
|
let mission_bytes = read_vfs(&vfs, &mission_path)?;
|
||||||
|
|
||||||
record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Map);
|
|
||||||
let land_path =
|
let land_path =
|
||||||
decode_mission_land_path(&mission_bytes, TmaProfile::Strict).map_err(|source| {
|
decode_mission_land_path(&mission_bytes, TmaProfile::Strict).map_err(|source| {
|
||||||
EngineError::Mission {
|
EngineError::Mission {
|
||||||
@@ -919,7 +664,6 @@ fn load_mission_with_options_and_progress(
|
|||||||
TerrainPreparationError::Runtime(source) => EngineError::Terrain(source),
|
TerrainPreparationError::Runtime(source) => EngineError::Terrain(source),
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Tma);
|
|
||||||
let mission = decode_mission_payload(mission_bytes, TmaProfile::Strict).map_err(|source| {
|
let mission = decode_mission_payload(mission_bytes, TmaProfile::Strict).map_err(|source| {
|
||||||
EngineError::Mission {
|
EngineError::Mission {
|
||||||
path: mission_path.as_str().to_string(),
|
path: mission_path.as_str().to_string(),
|
||||||
@@ -927,18 +671,6 @@ fn load_mission_with_options_and_progress(
|
|||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
let loaded_scripts = load_mission_scripts(&vfs, &mission)?;
|
let loaded_scripts = load_mission_scripts(&vfs, &mission)?;
|
||||||
trace.transforms = mission
|
|
||||||
.objects
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(object_index, object)| PlacedTransformProfile {
|
|
||||||
object_index,
|
|
||||||
position: object.position,
|
|
||||||
orientation_raw: object.orientation,
|
|
||||||
scale: object.scale,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Graph);
|
|
||||||
let repository = CachedResourceRepository::with_payload_cache_budget(
|
let repository = CachedResourceRepository::with_payload_cache_budget(
|
||||||
vfs.clone(),
|
vfs.clone(),
|
||||||
MISSION_DECODED_PAYLOAD_CACHE_ENTRIES,
|
MISSION_DECODED_PAYLOAD_CACHE_ENTRIES,
|
||||||
@@ -957,93 +689,34 @@ fn load_mission_with_options_and_progress(
|
|||||||
};
|
};
|
||||||
let (mut prototype_graph, resolved_prototypes, mut prototype_report) =
|
let (mut prototype_graph, resolved_prototypes, mut prototype_report) =
|
||||||
build_prototype_graph_report(&repository, vfs.as_ref(), scoped_graph_roots);
|
build_prototype_graph_report(&repository, vfs.as_ref(), scoped_graph_roots);
|
||||||
record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::GraphVisuals);
|
extend_graph_report_with_visual_dependencies(
|
||||||
let mut last_graph_visual_phase = None;
|
|
||||||
extend_graph_report_with_visual_dependencies_with_progress(
|
|
||||||
&repository,
|
&repository,
|
||||||
&mut prototype_report,
|
&mut prototype_report,
|
||||||
&mut prototype_graph,
|
&mut prototype_graph,
|
||||||
&resolved_prototypes,
|
&resolved_prototypes,
|
||||||
|progress| {
|
|
||||||
let runtime_phase = match progress.phase {
|
|
||||||
VisualDependencyPhase::Wear => MissionLoadPhase::GraphVisualWears,
|
|
||||||
VisualDependencyPhase::Material => MissionLoadPhase::GraphVisualMaterials,
|
|
||||||
VisualDependencyPhase::Texture => MissionLoadPhase::GraphVisualTextures,
|
|
||||||
};
|
|
||||||
if last_graph_visual_phase != Some(runtime_phase) {
|
|
||||||
record_load_phase(&mut trace, &mut on_phase, runtime_phase);
|
|
||||||
last_graph_visual_phase = Some(runtime_phase);
|
|
||||||
}
|
|
||||||
if progress.request_count > 1 {
|
|
||||||
let request_phase = match progress.phase {
|
|
||||||
VisualDependencyPhase::Wear => None,
|
|
||||||
VisualDependencyPhase::Material => {
|
|
||||||
Some(MissionLoadPhase::GraphVisualMaterialRequests {
|
|
||||||
request_count: progress.request_count,
|
|
||||||
graph_node_count: progress.graph_node_count,
|
|
||||||
graph_edge_count: progress.graph_edge_count,
|
|
||||||
wear_cache_entries: progress.cache_entries.wear_entries,
|
|
||||||
material_cache_entries: progress.cache_entries.material_entries,
|
|
||||||
texture_cache_entries: progress.cache_entries.texture_entries,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
VisualDependencyPhase::Texture => {
|
|
||||||
Some(MissionLoadPhase::GraphVisualTextureRequests {
|
|
||||||
request_count: progress.request_count,
|
|
||||||
graph_node_count: progress.graph_node_count,
|
|
||||||
graph_edge_count: progress.graph_edge_count,
|
|
||||||
wear_cache_entries: progress.cache_entries.wear_entries,
|
|
||||||
material_cache_entries: progress.cache_entries.material_entries,
|
|
||||||
texture_cache_entries: progress.cache_entries.texture_entries,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Some(request_phase) = request_phase {
|
|
||||||
record_load_phase(&mut trace, &mut on_phase, request_phase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
if !prototype_report.is_success() {
|
if !prototype_report.is_success() {
|
||||||
return Err(EngineError::PrototypeGraph {
|
return Err(EngineError::PrototypeGraph {
|
||||||
failures: prototype_report.failures.clone(),
|
failures: prototype_report.failures.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Assets);
|
|
||||||
let asset_manager = AssetManager::new(repository);
|
let asset_manager = AssetManager::new(repository);
|
||||||
let mut last_asset_phase = None;
|
|
||||||
let mut observe_asset_phase = |phase| {
|
|
||||||
let runtime_phase = match phase {
|
|
||||||
AssetPreparationPhase::ModelMesh => MissionLoadPhase::AssetModelMeshes,
|
|
||||||
AssetPreparationPhase::WearTable => MissionLoadPhase::AssetWearTables,
|
|
||||||
AssetPreparationPhase::Materials => MissionLoadPhase::AssetMaterials,
|
|
||||||
AssetPreparationPhase::Textures => MissionLoadPhase::AssetTextures,
|
|
||||||
};
|
|
||||||
if last_asset_phase != Some(runtime_phase) {
|
|
||||||
record_load_phase(&mut trace, &mut on_phase, runtime_phase);
|
|
||||||
last_asset_phase = Some(runtime_phase);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mission_assets = match options.asset_scope {
|
let mission_assets = match options.asset_scope {
|
||||||
MissionAssetScope::Full => asset_manager.prepare_mission_assets_with_progress(
|
MissionAssetScope::Full => asset_manager.prepare_mission_assets(
|
||||||
&prototype_graph.root_prototype_request_spans,
|
&prototype_graph.root_prototype_request_spans,
|
||||||
&resolved_prototypes,
|
&resolved_prototypes,
|
||||||
&mut observe_asset_phase,
|
|
||||||
),
|
),
|
||||||
MissionAssetScope::FirstMeshPreview => prepare_first_preview_assets(
|
MissionAssetScope::FirstMeshPreview => prepare_first_preview_assets(
|
||||||
&asset_manager,
|
&asset_manager,
|
||||||
&prototype_graph.root_prototype_request_spans,
|
&prototype_graph.root_prototype_request_spans,
|
||||||
&resolved_prototypes,
|
&resolved_prototypes,
|
||||||
&mut observe_asset_phase,
|
|
||||||
),
|
),
|
||||||
MissionAssetScope::PreviewRoots(root_count) => asset_manager
|
MissionAssetScope::PreviewRoots(root_count) => asset_manager.prepare_mission_assets(
|
||||||
.prepare_mission_assets_with_progress(
|
&prototype_graph.root_prototype_request_spans[..root_count
|
||||||
&prototype_graph.root_prototype_request_spans[..root_count
|
.get()
|
||||||
.get()
|
.min(prototype_graph.root_prototype_request_spans.len())],
|
||||||
.min(prototype_graph.root_prototype_request_spans.len())],
|
&resolved_prototypes,
|
||||||
&resolved_prototypes,
|
),
|
||||||
&mut observe_asset_phase,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
.map_err(|source| EngineError::AssetPreparation {
|
.map_err(|source| EngineError::AssetPreparation {
|
||||||
mission: request.key.clone(),
|
mission: request.key.clone(),
|
||||||
@@ -1079,7 +752,6 @@ fn load_mission_with_options_and_progress(
|
|||||||
.collect();
|
.collect();
|
||||||
let mut new_runtime_world = new_world(WorldConfig);
|
let mut new_runtime_world = new_world(WorldConfig);
|
||||||
let mut handles = Vec::with_capacity(mission.objects.len());
|
let mut handles = Vec::with_capacity(mission.objects.len());
|
||||||
record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Construct);
|
|
||||||
for (index, object) in mission.objects.iter().enumerate() {
|
for (index, object) in mission.objects.iter().enumerate() {
|
||||||
let original_id = u32::try_from(index).ok().map(OriginalObjectId);
|
let original_id = u32::try_from(index).ok().map(OriginalObjectId);
|
||||||
let handle = construct_object(&mut new_runtime_world, ObjectDraft { original_id })?;
|
let handle = construct_object(&mut new_runtime_world, ObjectDraft { original_id })?;
|
||||||
@@ -1094,19 +766,18 @@ fn load_mission_with_options_and_progress(
|
|||||||
)?;
|
)?;
|
||||||
handles.push(handle);
|
handles.push(handle);
|
||||||
}
|
}
|
||||||
trace.drafts_before_registration = handles.len();
|
let mut registered_objects = 0;
|
||||||
record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Register);
|
|
||||||
for handle in &handles {
|
for handle in &handles {
|
||||||
if options.fail_after_registered_objects == Some(trace.registered_objects) {
|
if options.fail_after_registered_objects == Some(registered_objects) {
|
||||||
let report = fparkan_world::shutdown(new_runtime_world);
|
let report = fparkan_world::shutdown(new_runtime_world);
|
||||||
return Err(EngineError::RegistrationTeardown {
|
return Err(EngineError::RegistrationTeardown {
|
||||||
registered_objects: trace.registered_objects,
|
registered_objects,
|
||||||
released_objects: report.released_objects.len(),
|
released_objects: report.released_objects.len(),
|
||||||
managers_released: report.managers_released,
|
managers_released: report.managers_released,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
register_object(&mut new_runtime_world, *handle)?;
|
register_object(&mut new_runtime_world, *handle)?;
|
||||||
trace.registered_objects += 1;
|
registered_objects += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let summary = LoadedMission {
|
let summary = LoadedMission {
|
||||||
@@ -1174,32 +845,17 @@ fn load_mission_with_options_and_progress(
|
|||||||
script_varset_states: loaded_scripts.varset_states,
|
script_varset_states: loaded_scripts.varset_states,
|
||||||
script_outcome: MissionScriptOutcome::Active,
|
script_outcome: MissionScriptOutcome::Active,
|
||||||
});
|
});
|
||||||
Ok((summary, trace))
|
Ok(summary)
|
||||||
}
|
|
||||||
|
|
||||||
fn record_load_phase(
|
|
||||||
trace: &mut MissionLoadTrace,
|
|
||||||
on_phase: &mut Option<&mut dyn FnMut(MissionLoadPhase)>,
|
|
||||||
phase: MissionLoadPhase,
|
|
||||||
) {
|
|
||||||
trace.phases.push(phase);
|
|
||||||
if let Some(observer) = on_phase.as_deref_mut() {
|
|
||||||
observer(phase);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_first_preview_assets<R: ResourceRepository>(
|
fn prepare_first_preview_assets<R: ResourceRepository>(
|
||||||
asset_manager: &AssetManager<R>,
|
asset_manager: &AssetManager<R>,
|
||||||
root_spans: &[std::ops::Range<usize>],
|
root_spans: &[std::ops::Range<usize>],
|
||||||
prototypes: &[fparkan_prototype::EffectivePrototype],
|
prototypes: &[fparkan_prototype::EffectivePrototype],
|
||||||
on_phase: &mut dyn FnMut(AssetPreparationPhase),
|
|
||||||
) -> Result<MissionAssets, AssetPreparationError> {
|
) -> Result<MissionAssets, AssetPreparationError> {
|
||||||
for span in root_spans {
|
for span in root_spans {
|
||||||
let assets = asset_manager.prepare_mission_assets_with_progress(
|
let assets =
|
||||||
std::slice::from_ref(span),
|
asset_manager.prepare_mission_assets(std::slice::from_ref(span), prototypes)?;
|
||||||
prototypes,
|
|
||||||
&mut *on_phase,
|
|
||||||
)?;
|
|
||||||
if !assets.models.is_empty() {
|
if !assets.models.is_empty() {
|
||||||
return Ok(assets);
|
return Ok(assets);
|
||||||
}
|
}
|
||||||
@@ -1216,7 +872,9 @@ pub fn step_headless(
|
|||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
input: InputSnapshot,
|
input: InputSnapshot,
|
||||||
) -> Result<FrameResult, EngineError> {
|
) -> Result<FrameResult, EngineError> {
|
||||||
run_frame(engine, input, SchedulerPresentation::Headless)
|
Ok(FrameResult {
|
||||||
|
snapshot: step(&mut engine.world, &input)?,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Steps rendered mode.
|
/// Steps rendered mode.
|
||||||
@@ -1225,20 +883,7 @@ pub fn step_headless(
|
|||||||
///
|
///
|
||||||
/// Returns [`EngineError`] when the world step fails.
|
/// Returns [`EngineError`] when the world step fails.
|
||||||
pub fn frame(engine: &mut Engine) -> Result<FrameResult, EngineError> {
|
pub fn frame(engine: &mut Engine) -> Result<FrameResult, EngineError> {
|
||||||
match engine.config.mode {
|
step_headless(engine, InputSnapshot)
|
||||||
EngineMode::Headless => step_headless(engine, InputSnapshot),
|
|
||||||
EngineMode::Rendered => run_frame(engine, InputSnapshot, SchedulerPresentation::Rendered),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shuts down engine.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Currently shutdown is infallible, but the `Result` preserves the lifecycle
|
|
||||||
/// API for future service teardown failures.
|
|
||||||
pub fn shutdown(_engine: Engine) -> Result<(), EngineError> {
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the loaded mission summary.
|
/// Returns the loaded mission summary.
|
||||||
@@ -1451,78 +1096,6 @@ pub fn loaded_mission_object_drafts(engine: &Engine) -> Option<&[MissionObjectDr
|
|||||||
.map(|state| state.object_drafts.as_slice())
|
.map(|state| state.object_drafts.as_slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
enum SchedulerPresentation {
|
|
||||||
Headless,
|
|
||||||
Rendered,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
struct Scheduler {
|
|
||||||
phase: Option<SchedulerPhase>,
|
|
||||||
trace: FrameTrace,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Scheduler {
|
|
||||||
fn enter(&mut self, phase: SchedulerPhase) -> Result<(), EngineError> {
|
|
||||||
if let Some(previous) = self.phase {
|
|
||||||
if scheduler_phase_index(phase) <= scheduler_phase_index(previous) {
|
|
||||||
return Err(EngineError::SchedulerPhaseOrder {
|
|
||||||
previous,
|
|
||||||
current: phase,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.phase = Some(phase);
|
|
||||||
self.trace.phases.push(phase);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finish(self) -> FrameTrace {
|
|
||||||
self.trace
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_frame(
|
|
||||||
engine: &mut Engine,
|
|
||||||
input: InputSnapshot,
|
|
||||||
presentation: SchedulerPresentation,
|
|
||||||
) -> Result<FrameResult, EngineError> {
|
|
||||||
let mut scheduler = Scheduler::default();
|
|
||||||
scheduler.enter(SchedulerPhase::CollectPlatformEvents)?;
|
|
||||||
scheduler.enter(SchedulerPhase::BuildInputSnapshot)?;
|
|
||||||
scheduler.enter(SchedulerPhase::AdvanceGameClock)?;
|
|
||||||
scheduler.enter(SchedulerPhase::CalculateWorldQueue)?;
|
|
||||||
let snapshot = step(&mut engine.world, &input)?;
|
|
||||||
scheduler.enter(SchedulerPhase::ApplyDeferredOperations)?;
|
|
||||||
scheduler.enter(SchedulerPhase::UpdateAnimationAndEffects)?;
|
|
||||||
if presentation == SchedulerPresentation::Rendered {
|
|
||||||
scheduler.enter(SchedulerPhase::PublishRenderSnapshot)?;
|
|
||||||
scheduler.enter(SchedulerPhase::RenderWorld)?;
|
|
||||||
}
|
|
||||||
scheduler.enter(SchedulerPhase::EndFrameCallbacks)?;
|
|
||||||
scheduler.enter(SchedulerPhase::Maintenance)?;
|
|
||||||
Ok(FrameResult {
|
|
||||||
snapshot,
|
|
||||||
trace: scheduler.finish(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scheduler_phase_index(phase: SchedulerPhase) -> u8 {
|
|
||||||
match phase {
|
|
||||||
SchedulerPhase::CollectPlatformEvents => 0,
|
|
||||||
SchedulerPhase::BuildInputSnapshot => 1,
|
|
||||||
SchedulerPhase::AdvanceGameClock => 2,
|
|
||||||
SchedulerPhase::CalculateWorldQueue => 3,
|
|
||||||
SchedulerPhase::ApplyDeferredOperations => 4,
|
|
||||||
SchedulerPhase::UpdateAnimationAndEffects => 5,
|
|
||||||
SchedulerPhase::PublishRenderSnapshot => 6,
|
|
||||||
SchedulerPhase::RenderWorld => 7,
|
|
||||||
SchedulerPhase::EndFrameCallbacks => 8,
|
|
||||||
SchedulerPhase::Maintenance => 9,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normalize_engine_path(role: &'static str, value: &str) -> Result<NormalizedPath, EngineError> {
|
fn normalize_engine_path(role: &'static str, value: &str) -> Result<NormalizedPath, EngineError> {
|
||||||
normalize_relative(value.as_bytes(), PathPolicy::StrictLegacy).map_err(|source| {
|
normalize_relative(value.as_bytes(), PathPolicy::StrictLegacy).map_err(|source| {
|
||||||
EngineError::Path {
|
EngineError::Path {
|
||||||
@@ -1702,7 +1275,7 @@ fn handler19_x87_truncated_u32(
|
|||||||
}
|
}
|
||||||
// ai.dll's __ftol saves the x87 control word, sets rounding-control bits
|
// ai.dll's __ftol saves the x87 control word, sets rounding-control bits
|
||||||
// to truncate, performs `fistp qword`, and restores the control word.
|
// to truncate, performs `fistp qword`, and restores the control word.
|
||||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
|
||||||
let word = value.trunc() as u32;
|
let word = value.trunc() as u32;
|
||||||
Ok(word)
|
Ok(word)
|
||||||
}
|
}
|
||||||
@@ -1758,13 +1331,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn load_mission_requires_vfs_and_keeps_world_unchanged_on_error() {
|
fn load_mission_requires_vfs_and_keeps_world_unchanged_on_error() {
|
||||||
let mut engine = create(
|
let mut engine = create(EngineServices::default()).expect("engine");
|
||||||
EngineConfig {
|
|
||||||
mode: EngineMode::Headless,
|
|
||||||
},
|
|
||||||
EngineServices::default(),
|
|
||||||
)
|
|
||||||
.expect("engine");
|
|
||||||
let before = step_headless(&mut engine, InputSnapshot).expect("step");
|
let before = step_headless(&mut engine, InputSnapshot).expect("step");
|
||||||
let err = load_mission(
|
let err = load_mission(
|
||||||
&mut engine,
|
&mut engine,
|
||||||
@@ -1778,30 +1345,6 @@ mod tests {
|
|||||||
assert_eq!(before.snapshot.objects, after.snapshot.objects);
|
assert_eq!(before.snapshot.objects, after.snapshot.objects);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn load_progress_reports_context_before_missing_vfs_error() {
|
|
||||||
let mut engine = create(
|
|
||||||
EngineConfig {
|
|
||||||
mode: EngineMode::Headless,
|
|
||||||
},
|
|
||||||
EngineServices::default(),
|
|
||||||
)
|
|
||||||
.expect("engine");
|
|
||||||
let mut phases = Vec::new();
|
|
||||||
|
|
||||||
let err = load_mission_with_progress(
|
|
||||||
&mut engine,
|
|
||||||
MissionRequest {
|
|
||||||
key: "MISSIONS/Autodemo.00/data.tma".to_string(),
|
|
||||||
},
|
|
||||||
|phase| phases.push(phase),
|
|
||||||
)
|
|
||||||
.expect_err("missing VFS");
|
|
||||||
|
|
||||||
assert!(matches!(err, EngineError::MissingVfs));
|
|
||||||
assert_eq!(phases, vec![MissionLoadPhase::Context]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn script_varset_prefers_bundle_adjacent_file_then_uses_shared_fallback() {
|
fn script_varset_prefers_bundle_adjacent_file_then_uses_shared_fallback() {
|
||||||
let mut personal_files = MemoryVfs::default();
|
let mut personal_files = MemoryVfs::default();
|
||||||
@@ -1996,88 +1539,9 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn headless_scheduler_trace_skips_presentation_phases() {
|
|
||||||
let mut engine = create(
|
|
||||||
EngineConfig {
|
|
||||||
mode: EngineMode::Headless,
|
|
||||||
},
|
|
||||||
EngineServices::default(),
|
|
||||||
)
|
|
||||||
.expect("engine");
|
|
||||||
|
|
||||||
let result = frame(&mut engine).expect("frame");
|
|
||||||
|
|
||||||
assert_eq!(result.snapshot.tick.0, 1);
|
|
||||||
assert_eq!(
|
|
||||||
result.trace.phases,
|
|
||||||
vec![
|
|
||||||
SchedulerPhase::CollectPlatformEvents,
|
|
||||||
SchedulerPhase::BuildInputSnapshot,
|
|
||||||
SchedulerPhase::AdvanceGameClock,
|
|
||||||
SchedulerPhase::CalculateWorldQueue,
|
|
||||||
SchedulerPhase::ApplyDeferredOperations,
|
|
||||||
SchedulerPhase::UpdateAnimationAndEffects,
|
|
||||||
SchedulerPhase::EndFrameCallbacks,
|
|
||||||
SchedulerPhase::Maintenance,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn rendered_scheduler_trace_includes_presentation_after_simulation() {
|
|
||||||
let mut engine = create(
|
|
||||||
EngineConfig {
|
|
||||||
mode: EngineMode::Rendered,
|
|
||||||
},
|
|
||||||
EngineServices::default(),
|
|
||||||
)
|
|
||||||
.expect("engine");
|
|
||||||
|
|
||||||
let result = frame(&mut engine).expect("frame");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
result.trace.phases,
|
|
||||||
vec![
|
|
||||||
SchedulerPhase::CollectPlatformEvents,
|
|
||||||
SchedulerPhase::BuildInputSnapshot,
|
|
||||||
SchedulerPhase::AdvanceGameClock,
|
|
||||||
SchedulerPhase::CalculateWorldQueue,
|
|
||||||
SchedulerPhase::ApplyDeferredOperations,
|
|
||||||
SchedulerPhase::UpdateAnimationAndEffects,
|
|
||||||
SchedulerPhase::PublishRenderSnapshot,
|
|
||||||
SchedulerPhase::RenderWorld,
|
|
||||||
SchedulerPhase::EndFrameCallbacks,
|
|
||||||
SchedulerPhase::Maintenance,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn scheduler_rejects_phase_regressions() {
|
|
||||||
let mut scheduler = Scheduler::default();
|
|
||||||
scheduler
|
|
||||||
.enter(SchedulerPhase::BuildInputSnapshot)
|
|
||||||
.expect("enter build input");
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
scheduler.enter(SchedulerPhase::CollectPlatformEvents),
|
|
||||||
Err(EngineError::SchedulerPhaseOrder {
|
|
||||||
previous: SchedulerPhase::BuildInputSnapshot,
|
|
||||||
current: SchedulerPhase::CollectPlatformEvents,
|
|
||||||
})
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reference_movement_rejects_non_finite_or_non_positive_input() {
|
fn reference_movement_rejects_non_finite_or_non_positive_input() {
|
||||||
let mut engine = create(
|
let mut engine = create(EngineServices::default()).expect("engine");
|
||||||
EngineConfig {
|
|
||||||
mode: EngineMode::Headless,
|
|
||||||
},
|
|
||||||
EngineServices::default(),
|
|
||||||
)
|
|
||||||
.expect("engine");
|
|
||||||
|
|
||||||
let err =
|
let err =
|
||||||
advance_reference_movement(&mut engine, OriginalObjectId(0), [f32::NAN, 0.0], 1.0)
|
advance_reference_movement(&mut engine, OriginalObjectId(0), [f32::NAN, 0.0], 1.0)
|
||||||
@@ -2093,61 +1557,12 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore = "requires licensed corpus"]
|
|
||||||
fn load_trace_records_preparation_before_registration_and_raw_transforms() {
|
|
||||||
let root = licensed_root("IS");
|
|
||||||
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(&root));
|
|
||||||
let mut engine = create(
|
|
||||||
EngineConfig {
|
|
||||||
mode: EngineMode::Headless,
|
|
||||||
},
|
|
||||||
EngineServices::new(vfs),
|
|
||||||
)
|
|
||||||
.expect("engine");
|
|
||||||
|
|
||||||
let (loaded, trace) = load_mission_with_trace(
|
|
||||||
&mut engine,
|
|
||||||
MissionRequest {
|
|
||||||
key: "MISSIONS/Autodemo.00/data.tma".to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("load mission with trace");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
trace.phases,
|
|
||||||
vec![
|
|
||||||
MissionLoadPhase::Context,
|
|
||||||
MissionLoadPhase::Map,
|
|
||||||
MissionLoadPhase::Tma,
|
|
||||||
MissionLoadPhase::Graph,
|
|
||||||
MissionLoadPhase::GraphVisuals,
|
|
||||||
MissionLoadPhase::Assets,
|
|
||||||
MissionLoadPhase::Construct,
|
|
||||||
MissionLoadPhase::Register,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
assert_eq!(trace.drafts_before_registration, loaded.object_count);
|
|
||||||
assert_eq!(trace.registered_objects, loaded.object_count);
|
|
||||||
assert_eq!(trace.transforms.len(), loaded.object_count);
|
|
||||||
assert!(trace.transforms.iter().all(|transform| transform
|
|
||||||
.orientation_raw
|
|
||||||
.iter()
|
|
||||||
.all(|component| component.is_finite())));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "requires licensed corpus"]
|
#[ignore = "requires licensed corpus"]
|
||||||
fn reference_movement_snaps_a_live_mission_object_to_terrain() {
|
fn reference_movement_snaps_a_live_mission_object_to_terrain() {
|
||||||
let root = licensed_root("IS");
|
let root = licensed_root("IS");
|
||||||
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(&root));
|
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(&root));
|
||||||
let mut engine = create(
|
let mut engine = create(EngineServices::new(vfs)).expect("engine");
|
||||||
EngineConfig {
|
|
||||||
mode: EngineMode::Headless,
|
|
||||||
},
|
|
||||||
EngineServices::new(vfs),
|
|
||||||
)
|
|
||||||
.expect("engine");
|
|
||||||
load_mission(
|
load_mission(
|
||||||
&mut engine,
|
&mut engine,
|
||||||
MissionRequest {
|
MissionRequest {
|
||||||
@@ -2221,13 +1636,7 @@ mod tests {
|
|||||||
inner: DirectoryVfs::new(&root),
|
inner: DirectoryVfs::new(&root),
|
||||||
denied,
|
denied,
|
||||||
});
|
});
|
||||||
let mut engine = create(
|
let mut engine = create(EngineServices::new(vfs)).expect("engine");
|
||||||
EngineConfig {
|
|
||||||
mode: EngineMode::Headless,
|
|
||||||
},
|
|
||||||
EngineServices::new(vfs),
|
|
||||||
)
|
|
||||||
.expect("engine");
|
|
||||||
let before = step_headless(&mut engine, InputSnapshot).expect("before");
|
let before = step_headless(&mut engine, InputSnapshot).expect("before");
|
||||||
let err = load_mission(&mut engine, mission).expect_err("load error");
|
let err = load_mission(&mut engine, mission).expect_err("load error");
|
||||||
match denied {
|
match denied {
|
||||||
@@ -2248,16 +1657,10 @@ mod tests {
|
|||||||
fn registration_phase_failure_uses_normal_teardown_and_keeps_engine_world() {
|
fn registration_phase_failure_uses_normal_teardown_and_keeps_engine_world() {
|
||||||
let root = licensed_root("IS");
|
let root = licensed_root("IS");
|
||||||
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
|
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
|
||||||
let mut engine = create(
|
let mut engine = create(EngineServices::new(vfs)).expect("engine");
|
||||||
EngineConfig {
|
|
||||||
mode: EngineMode::Headless,
|
|
||||||
},
|
|
||||||
EngineServices::new(vfs),
|
|
||||||
)
|
|
||||||
.expect("engine");
|
|
||||||
let before = step_headless(&mut engine, InputSnapshot).expect("before");
|
let before = step_headless(&mut engine, InputSnapshot).expect("before");
|
||||||
|
|
||||||
let err = load_mission_with_options_and_progress(
|
let err = load_mission_with_options(
|
||||||
&mut engine,
|
&mut engine,
|
||||||
MissionRequest {
|
MissionRequest {
|
||||||
key: "MISSIONS/CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma".to_string(),
|
key: "MISSIONS/CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma".to_string(),
|
||||||
@@ -2266,7 +1669,6 @@ mod tests {
|
|||||||
fail_after_registered_objects: Some(1),
|
fail_after_registered_objects: Some(1),
|
||||||
..MissionLoadOptions::default()
|
..MissionLoadOptions::default()
|
||||||
},
|
},
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
.expect_err("forced registration failure");
|
.expect_err("forced registration failure");
|
||||||
|
|
||||||
@@ -2326,13 +1728,7 @@ mod tests {
|
|||||||
] {
|
] {
|
||||||
let root = local_testdata_root(case.0);
|
let root = local_testdata_root(case.0);
|
||||||
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
|
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
|
||||||
let mut engine = create(
|
let mut engine = create(EngineServices::new(vfs)).expect("engine");
|
||||||
EngineConfig {
|
|
||||||
mode: EngineMode::Headless,
|
|
||||||
},
|
|
||||||
EngineServices::new(vfs),
|
|
||||||
)
|
|
||||||
.expect("engine");
|
|
||||||
let loaded = load_mission(
|
let loaded = load_mission(
|
||||||
&mut engine,
|
&mut engine,
|
||||||
MissionRequest {
|
MissionRequest {
|
||||||
@@ -2440,13 +1836,7 @@ mod tests {
|
|||||||
fn run_headless_case(case: HeadlessCase) -> WorldSnapshot {
|
fn run_headless_case(case: HeadlessCase) -> WorldSnapshot {
|
||||||
let root = licensed_root(case.root);
|
let root = licensed_root(case.root);
|
||||||
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
|
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
|
||||||
let mut engine = create(
|
let mut engine = create(EngineServices::new(vfs)).expect("engine");
|
||||||
EngineConfig {
|
|
||||||
mode: EngineMode::Headless,
|
|
||||||
},
|
|
||||||
EngineServices::new(vfs),
|
|
||||||
)
|
|
||||||
.expect("engine");
|
|
||||||
let loaded = load_mission(
|
let loaded = load_mission(
|
||||||
&mut engine,
|
&mut engine,
|
||||||
MissionRequest {
|
MissionRequest {
|
||||||
@@ -2474,13 +1864,7 @@ mod tests {
|
|||||||
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
|
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
|
||||||
let mut totals = LoadTotals::default();
|
let mut totals = LoadTotals::default();
|
||||||
for mission in missions {
|
for mission in missions {
|
||||||
let mut engine = create(
|
let mut engine = create(EngineServices::new(vfs.clone())).expect("engine");
|
||||||
EngineConfig {
|
|
||||||
mode: EngineMode::Headless,
|
|
||||||
},
|
|
||||||
EngineServices::new(vfs.clone()),
|
|
||||||
)
|
|
||||||
.expect("engine");
|
|
||||||
let loaded = load_mission(&mut engine, MissionRequest { key: mission })
|
let loaded = load_mission(&mut engine, MissionRequest { key: mission })
|
||||||
.expect("load mission foundation");
|
.expect("load mission foundation");
|
||||||
assert_eq!(loaded.object_count, loaded.registered_objects);
|
assert_eq!(loaded.object_count, loaded.registered_objects);
|
||||||
|
|||||||
@@ -6,7 +6,4 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
|
fparkan-binary = { path = "../fparkan-binary" }
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(test, allow(clippy::expect_used, clippy::panic, clippy::unwrap_used))]
|
|
||||||
//! Lossless, bounded reader for compiled AI `.scr` packages.
|
//! Lossless, bounded reader for compiled AI `.scr` packages.
|
||||||
//!
|
//!
|
||||||
//! This module preserves the layout proven by the GOG `ai.dll` reader. It
|
//! This module preserves the layout proven by the GOG `ai.dll` reader. It
|
||||||
@@ -1061,9 +1060,7 @@ impl ScriptInstruction {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn dispatch_selector(&self) -> ScriptDispatchSelector {
|
pub fn dispatch_selector(&self) -> ScriptDispatchSelector {
|
||||||
match self.header_words[0] {
|
match self.header_words[0] {
|
||||||
value if value < GOG_HANDLER_COUNT =>
|
value if value < GOG_HANDLER_COUNT => {
|
||||||
{
|
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
|
||||||
ScriptDispatchSelector::Handler(self.header_words[0] as u8)
|
ScriptDispatchSelector::Handler(self.header_words[0] as u8)
|
||||||
}
|
}
|
||||||
u32::MAX => ScriptDispatchSelector::Sentinel,
|
u32::MAX => ScriptDispatchSelector::Sentinel,
|
||||||
|
|||||||
@@ -6,8 +6,5 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
|
fparkan-binary = { path = "../fparkan-binary" }
|
||||||
fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
|
fparkan-nres = { path = "../fparkan-nres" }
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Terrain disk format primitives.
|
//! Terrain disk format primitives.
|
||||||
|
|
||||||
use fparkan_binary::{checked_count_bytes, Cursor, DecodeError};
|
use fparkan_binary::{checked_count_bytes, Cursor, DecodeError};
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fparkan-terrain-format = { path = "../fparkan-terrain-format", version = "0.1.0" }
|
fparkan-terrain-format = { path = "../fparkan-terrain-format" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
|
fparkan-nres = { path = "../fparkan-nres" }
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Validated terrain runtime queries using XY ground coordinates and Z height.
|
//! Validated terrain runtime queries using XY ground coordinates and Z height.
|
||||||
|
|
||||||
use fparkan_terrain_format::{FullSurfaceMask, LandMapDocument, LandMeshDocument};
|
use fparkan_terrain_format::{FullSurfaceMask, LandMapDocument, LandMeshDocument};
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "fparkan-test-support"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
repository.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
fparkan-render = { path = "../fparkan-render", version = "0.1.0" }
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#![forbid(unsafe_code)]
|
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Dev-only synthetic builders and fake ports.
|
|
||||||
|
|
||||||
use fparkan_render::{FrameOutput, RenderBackend, RenderCommandList, RenderError};
|
|
||||||
|
|
||||||
/// Fake clock.
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
||||||
pub struct FakeClock {
|
|
||||||
/// Current tick.
|
|
||||||
pub tick: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recording backend.
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
pub struct RecordingRenderBackend {
|
|
||||||
/// Recorded command lists.
|
|
||||||
pub captures: Vec<RenderCommandList>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderBackend for RecordingRenderBackend {
|
|
||||||
fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError> {
|
|
||||||
self.captures.push(commands.clone());
|
|
||||||
Ok(FrameOutput)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,4 @@ repository.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
|
fparkan-nres = { path = "../fparkan-nres" }
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Stage-3 Texm texture contract.
|
//! Stage-3 Texm texture contract.
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|||||||
@@ -6,8 +6,5 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
|
fparkan-binary = { path = "../fparkan-binary" }
|
||||||
fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
|
fparkan-path = { path = "../fparkan-path" }
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Virtual filesystem ports for resource loading.
|
//! Virtual filesystem ports for resource loading.
|
||||||
|
|
||||||
use fparkan_binary::{sha256, Sha256Digest};
|
use fparkan_binary::{sha256, Sha256Digest};
|
||||||
@@ -312,7 +293,7 @@ pub struct MemoryVfs {
|
|||||||
|
|
||||||
impl MemoryVfs {
|
impl MemoryVfs {
|
||||||
/// Inserts a file.
|
/// Inserts a file.
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
|
||||||
pub fn insert(&mut self, path: NormalizedPath, bytes: Arc<[u8]>) {
|
pub fn insert(&mut self, path: NormalizedPath, bytes: Arc<[u8]>) {
|
||||||
let path = path.as_bytes().to_vec();
|
let path = path.as_bytes().to_vec();
|
||||||
self.files.insert(path, bytes);
|
self.files.insert(path, bytes);
|
||||||
@@ -347,13 +328,13 @@ impl MemoryVfs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[allow(clippy::unnecessary_wraps)]
|
|
||||||
fn file_identity(metadata: &fs::Metadata) -> Option<u64> {
|
fn file_identity(metadata: &fs::Metadata) -> Option<u64> {
|
||||||
Some(metadata.dev().rotate_left(32) ^ metadata.ino())
|
Some(metadata.dev().rotate_left(32) ^ metadata.ino())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
#[allow(clippy::unnecessary_wraps)]
|
|
||||||
fn file_identity(metadata: &fs::Metadata) -> Option<u64> {
|
fn file_identity(metadata: &fs::Metadata) -> Option<u64> {
|
||||||
// Stable Rust does not expose the Windows file index. Keep a best-effort
|
// Stable Rust does not expose the Windows file index. Keep a best-effort
|
||||||
// replacement detector from metadata that is available through the
|
// replacement detector from metadata that is available through the
|
||||||
|
|||||||
@@ -6,7 +6,4 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
|
fparkan-binary = { path = "../fparkan-binary" }
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(
|
|
||||||
test,
|
|
||||||
allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::identity_op,
|
|
||||||
clippy::too_many_lines,
|
|
||||||
clippy::uninlined_format_args,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::needless_raw_string_hashes,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::unwrap_used
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
//! Deterministic world identity, queue, lifecycle, and snapshots.
|
//! Deterministic world identity, queue, lifecycle, and snapshots.
|
||||||
|
|
||||||
use fparkan_binary::sha256;
|
use fparkan_binary::sha256;
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
[graph]
|
|
||||||
all-features = true
|
|
||||||
|
|
||||||
[advisories]
|
|
||||||
yanked = "deny"
|
|
||||||
|
|
||||||
[bans]
|
|
||||||
multiple-versions = "allow"
|
|
||||||
wildcards = "deny"
|
|
||||||
deny = [
|
|
||||||
{ name = "native-tls" },
|
|
||||||
{ name = "openssl" },
|
|
||||||
{ name = "openssl-sys" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[licenses]
|
|
||||||
allow = [
|
|
||||||
"Apache-2.0",
|
|
||||||
"BSD-3-Clause",
|
|
||||||
"GPL-2.0-only",
|
|
||||||
"ISC",
|
|
||||||
"MIT",
|
|
||||||
"Unicode-3.0",
|
|
||||||
"Zlib",
|
|
||||||
]
|
|
||||||
|
|
||||||
[sources]
|
|
||||||
unknown-registry = "deny"
|
|
||||||
unknown-git = "deny"
|
|
||||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
|
||||||
@@ -98,42 +98,3 @@ typed schema свойств по consumers, а не по одному имени
|
|||||||
|
|
||||||
Такие ветки реализуются по бинарному коду и synthetic tests, а статус
|
Такие ветки реализуются по бинарному коду и synthetic tests, а статус
|
||||||
corpus-verified получают только после реального файла или runtime trace.
|
corpus-verified получают только после реального файла или runtime trace.
|
||||||
|
|
||||||
## Dynamic-stage requirements
|
|
||||||
|
|
||||||
Оставшиеся вопросы нельзя закрыть только статическими архивами. Нужна
|
|
||||||
изолированная 32-bit Windows-среда, неизменённые игровые каталоги, manifest
|
|
||||||
SHA-256, debugger, API/vtable hooks, controlled clocks/input и автоматический
|
|
||||||
launcher, который восстанавливает snapshot, запускает один test case, собирает
|
|
||||||
логи и завершает процесс без ручного вмешательства.
|
|
||||||
|
|
||||||
Для каждого capture сохраняются build profile, module hashes, mission/resource
|
|
||||||
key, configuration, device profile, initial state, input/time script и версии
|
|
||||||
инструментов.
|
|
||||||
|
|
||||||
## Local evidence requests
|
|
||||||
|
|
||||||
Поддерживаемая desktop-платформа — только Windows/Vulkan. Локальный smoke
|
|
||||||
`fparkan-vulkan-smoke` уже подтверждает настоящий Win32 surface/swapchain,
|
|
||||||
300 кадров, controlled resize и отсутствие validation warnings/errors.
|
|
||||||
Это закрывает фундамент Stage 0, но не доказывает визуальный паритет игры.
|
|
||||||
|
|
||||||
Следующие доказательства всё ещё нужны именно для Iron3D-compatible рендера:
|
|
||||||
|
|
||||||
- capability-gated capture из оригинального GOG процесса с camera/matrix,
|
|
||||||
draw/state и frame-boundary provenance;
|
|
||||||
- фиксированные Windows Vulkan captures статической модели, lightmapped модели
|
|
||||||
и terrain после совпадения backend-neutral command capture;
|
|
||||||
- затем controlled captures анимации, FX, прозрачности, теней и атмосферы.
|
|
||||||
|
|
||||||
Linux/macOS, GLES2, RG40XX и удалённые portability runners не являются
|
|
||||||
acceptance-гейтами этого проекта. Они не должны появляться в списке блокеров
|
|
||||||
или подменять Windows evidence.
|
|
||||||
|
|
||||||
## Closure criteria
|
|
||||||
|
|
||||||
Вопрос считается закрытым только при наличии build fingerprint, raw trace,
|
|
||||||
parser trace-а, минимального воспроизводимого input/resource/save/message,
|
|
||||||
формального контракта или явно ограниченной гипотезы, differential test для
|
|
||||||
изменённых DLL, обновления тематической главы и regression case, запускаемого
|
|
||||||
без ручного анализа.
|
|
||||||
|
|||||||
@@ -52,18 +52,11 @@ number, size, operands, control flow, effects, errors и минимальный
|
|||||||
GOG `ai.dll` доказывает этот framing двумя consumer-ами: loader по
|
GOG `ai.dll` доказывает этот framing двумя consumer-ами: loader по
|
||||||
`0x10001000` открывает `<bundle>.scr`, `varset.var`, `<bundle>.fml`, затем
|
`0x10001000` открывает `<bundle>.scr`, `varset.var`, `<bundle>.fml`, затем
|
||||||
собирает ровно 73 pointers handlers; `0x10011b20` читает описанную count-driven
|
собирает ровно 73 pointers handlers; `0x10011b20` читает описанную count-driven
|
||||||
структуру. Команда
|
структуру. В GOG `c1m2p.scr` reader находит 73 handler slots,
|
||||||
|
9 events, 17 nested records и 20 references, заканчивая на EOF.
|
||||||
|
Это подтверждает разбор пакета, но не смысл всех opcodes.
|
||||||
|
|
||||||
```powershell
|
Selector устроен следующим образом: loader `0x10001000` создаёт 73 pointers в
|
||||||
cargo run -p fparkan-cli -- script inspect `
|
|
||||||
'C:\GOG Games\Parkan - Iron Strategy\MISSIONS\SCRIPTS\c1m2p.scr' --format json
|
|
||||||
```
|
|
||||||
|
|
||||||
на исходном пакете возвращает `opcode_handler_count=73`, 9 events, 17 nested
|
|
||||||
records, 20 references и 0 trailing bytes. Это corpus evidence для reader-а,
|
|
||||||
но не разрешение на исполнение неизвестных 73 opcodes.
|
|
||||||
|
|
||||||
Теперь установлен selector: loader `0x10001000` создаёт 73 pointers в
|
|
||||||
фиксированном порядке, а `0x10011e70` копирует их без перестановки в runtime
|
фиксированном порядке, а `0x10011e70` копирует их без перестановки в runtime
|
||||||
array. Во всех 58 GOG `.scr` первый header word каждого nested record равен
|
array. Во всех 58 GOG `.scr` первый header word каждого nested record равен
|
||||||
`0..72` либо `0xffff_ffff`: соответственно 2095 handler selectors и 3992
|
`0..72` либо `0xffff_ffff`: соответственно 2095 handler selectors и 3992
|
||||||
@@ -157,9 +150,8 @@ slot2 одновременно в `+0x24/+0x28`, slot3 в `+0x2c` и slot6 в `+
|
|||||||
На границе mission runtime выбранный TMA clan `first_resource` теперь
|
На границе mission runtime выбранный TMA clan `first_resource` теперь
|
||||||
материализуется как отдельный `MissionScriptBundle`: loader нормализует
|
материализуется как отдельный `MissionScriptBundle`: loader нормализует
|
||||||
`<base>.scr`, декодирует его тем же bounded reader-ом и публикует immutable
|
`<base>.scr`, декодирует его тем же bounded reader-ом и публикует immutable
|
||||||
package вместе с clan provenance. Headless report выводит число таких packages
|
package вместе с clan provenance. Это именно wiring входных данных, не VM
|
||||||
и их named events. Это именно wiring входных данных, не VM execution: Init и
|
execution: Init и остальные events пока не dispatch-ятся, а ошибка чтения сохраняет
|
||||||
остальные events пока не dispatch-ятся, а ошибка чтения сохраняет
|
|
||||||
transactional rollback mission loader-а.
|
transactional rollback mission loader-а.
|
||||||
|
|
||||||
TMA properties остаются four raw `u32` words плюс имя, пока consumer/schema не
|
TMA properties остаются four raw `u32` words плюс имя, пока consumer/schema не
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user