Files
orisys-app/tools/scene_assets/compile_scene.py
T
ykcheungab b1cd37148c 删掉回放的五个转发、重复的空格快捷键与重复的等待循环。
回放调用点直接走 replay_ctl,场景工具的路径引导收敛到 _paths,panel_snapshot
复用 panel_check 的 _wait_for,report_from 收成模块私有的 _report_from。
2026-10-07 12:25:48 +08:00

272 lines
12 KiB
Python

"""Compile a legacy export into a runtime package (stages 2 and 5 of the 3D plan).
Offline only: this is the tool that bakes the coordinate transform, the unit
scale, the UV sampling table and the identity hashes into one file, so the runtime
never repeats that work. Stage 5 adds the three release protections the design
document asks for, and each is optional:
* ``--sign-key``: sign the package with an Ed25519 private key that stays on the
build machine. The client carries only the matching public key.
* ``--encrypt-key-id`` + ``--key-file``: additionally seal the content with
AES-256-GCM. Encryption never replaces signing; it is added to it.
* ``--lock-*`` (or ``--lock-from-probe``): record where the package may be
displayed. A release compiled for a GPU must not silently render on llvmpipe.
Units are an argument, never a default. The legacy mesh is 15.8 x 15.0 x 7.5 source
units; whether one of those is a millimetre is a physical claim about the sensor,
and ``validate_scene.py`` prints the measured candidates so a person can decide.
Examples::
# an engineering package: plain, unsigned, unlocked
python tools/scene_assets/compile_scene.py \\
--legacy-dir sensors/finger/assets/30fps-0703 \\
--map sensors/finger/assets/30fps-0703/unroll_map_uv_s0.4.npz \\
--meters-per-source-unit 0.001 \\
--output build/finger-display-1.0.0.oryscene
# a release package: signed, sealed, locked to the machine we probed
python tools/scene_assets/compile_scene.py \\
--legacy-dir sensors/finger/assets/30fps-0703 \\
--map sensors/finger/assets/30fps-0703/unroll_map_uv_s0.4.npz \\
--meters-per-source-unit 0.001 \\
--sign-key keys/build-ed25519.pem --sign-key-id build-2024 \\
--encrypt-key-id finger-display --key-file keys/finger-display.key \\
--lock-from-probe docs/architecture/gl-report-windows-dev.json \\
--output build/finger-display-1.0.0.oryscene
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
# Tests load this tool by file location, so add our own directory to the path.
if str(Path(__file__).resolve().parent) not in sys.path:
sys.path.insert(0, str(Path(__file__).resolve().parent))
from _paths import bootstrap_paths # noqa: E402
def _platform_lock_from_args(args):
"""The lock the command line describes, from a probe report or from flags."""
from orisys_app_shared.scene3d.contracts import PlatformLock
if args.lock_from_probe is not None:
report = json.loads(Path(args.lock_from_probe).read_text(encoding="utf-8"))
context = report.get("context") or {}
host = report.get("host") or {}
os_family = host.get("os_family") or host.get("system") or ""
machine = host.get("machine") or ""
return PlatformLock(
os_family=(str(os_family).lower(),) if os_family else (),
machine=(str(machine).lower(),) if machine else (),
gl_profile=(
str(context.get("profile_used")) if context.get("profile_used") else None
),
allowed_vendors=(str(context["vendor"]),) if context.get("vendor") else (),
allowed_renderers=(str(context["renderer"]),) if context.get("renderer") else (),
forbid_software_renderer=not args.allow_software_renderer,
min_texture_size=int(context.get("max_texture_size") or 0),
note=str(args.lock_note or f"qualified from {Path(args.lock_from_probe).name}"),
)
lock = PlatformLock(
os_family=tuple(v.lower() for v in (args.lock_os or ())),
machine=tuple(v.lower() for v in (args.lock_machine or ())),
gl_profile=args.lock_gl_profile,
allowed_vendors=tuple(args.lock_vendor or ()),
allowed_renderers=tuple(args.lock_renderer or ()),
forbid_software_renderer=not args.allow_software_renderer,
min_texture_size=int(args.lock_min_texture or 0),
note=str(args.lock_note or ""),
)
return None if lock.is_unrestricted() and not args.lock_note else lock
def _sealing_from_args(args):
"""The key material the command line points at, or ``None`` for a plain package."""
from orisys_app_shared.scene3d import protect
from orisys_app_shared.scene3d.contracts import Sealing
signing_pem = None
if args.sign_key is not None:
path = Path(args.sign_key)
if not path.is_file():
raise SystemExit(f"the signing key is missing: {path}")
signing_pem = path.read_bytes()
# Prove the key is usable before spending a compile on it.
protect.public_key_pem(signing_pem)
provider = None
if args.encrypt_key_id:
if args.key_file is None:
raise SystemExit("--encrypt-key-id also needs --key-file")
provider = protect.load_key_file(Path(args.key_file), key_id=args.encrypt_key_id)
if signing_pem is None and provider is None:
return None
verify = protect.public_key_pem(signing_pem) if signing_pem is not None else None
if verify is not None and args.write_verify_key is not None:
# The client's half of the pair. Publishing it is part of the release, and
# writing it here keeps it from being re-derived differently somewhere else.
target = Path(args.write_verify_key)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(verify)
return Sealing(
key_id=str(args.encrypt_key_id or ""),
provider=provider,
signing_key_pem=signing_pem,
signing_key_reference=str(args.sign_key_id or "build-key"),
verify_key_pem=verify,
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--legacy-dir", type=Path, required=True, help="legacy NPZ export")
parser.add_argument("--map", type=Path, required=True, help="field map (npz)")
parser.add_argument(
"--meters-per-source-unit",
type=float,
required=True,
help="stated unit scale (the importer never guesses it)",
)
parser.add_argument("--output", type=Path, required=True, help="package to write")
parser.add_argument("--asset-id", default="finger-display")
parser.add_argument("--version", default="1.0.0")
parser.add_argument("--lod-id", default="lod0")
parser.add_argument("--sensor-model", default="finger")
parser.add_argument(
"--transform",
type=Path,
default=None,
help="optional 4x4 rigid transform (npy) from source to sensor coordinates",
)
parser.add_argument(
"--housing",
type=Path,
default=None,
help=(
"optional housing/frame npz (vertices, faces) in the same source frame and "
"units as the export's mesh.npz; omitted, <legacy-dir>/housing.npz is used when "
"present. A housing exported in a different frame must be aligned first with "
"tools/scene_assets/align_housing.py"
),
)
protection = parser.add_argument_group(
"release protection (stage 5)", "all optional; a plain package is the default"
)
protection.add_argument("--sign-key", type=Path, default=None, help="Ed25519 private key (PEM)")
protection.add_argument("--sign-key-id", default="", help="name recorded in the envelope")
protection.add_argument(
"--write-verify-key",
type=Path,
default=None,
help="write the public half of --sign-key here (this is what the client ships)",
)
protection.add_argument("--encrypt-key-id", default="", help="content key id to seal with")
protection.add_argument("--key-file", type=Path, default=None, help="key for --encrypt-key-id")
protection.add_argument(
"--lock-from-probe",
type=Path,
default=None,
help="take the platform lock from a probe_gl.py report",
)
protection.add_argument("--lock-os", action="append", default=None, help="repeatable")
protection.add_argument("--lock-machine", action="append", default=None, help="repeatable")
protection.add_argument("--lock-gl-profile", default=None, choices=(None, "desktop33", "gles30"))
protection.add_argument("--lock-vendor", action="append", default=None, help="repeatable")
protection.add_argument("--lock-renderer", action="append", default=None, help="repeatable")
protection.add_argument("--lock-min-texture", type=int, default=0)
protection.add_argument("--lock-note", default="")
protection.add_argument(
"--allow-software-renderer",
action="store_true",
help="do not forbid a software rasteriser (the design document says do)",
)
parser.add_argument("--json", action="store_true")
args = parser.parse_args(argv)
bootstrap_paths()
import numpy as np
from orisys_app_shared.scene3d.contracts import ThreeDError
from orisys_app_shared.scene3d.legacy import LegacyFingerImporter
from orisys_app_shared.scene3d.package import describe_envelope, write_package
transform = (
np.load(args.transform, allow_pickle=False)
if args.transform is not None
else np.eye(4)
)
try:
sealing = _sealing_from_args(args)
lock = _platform_lock_from_args(args)
except ThreeDError as exc:
print(f"rejected: {exc.diagnostic.code}: {exc.diagnostic.message}", file=sys.stderr)
return 2
try:
assets = LegacyFingerImporter().convert(
args.legacy_dir,
args.map,
transform_sensor_from_source=transform,
meters_per_source_unit=args.meters_per_source_unit,
asset_id=args.asset_id,
asset_version=args.version,
lod_id=args.lod_id,
housing_path=args.housing,
)
if lock is not None:
from dataclasses import replace
# Part of the asset, so it travels into the manifest and the signature.
assets = replace(assets, platform_lock=lock)
report = write_package(
assets, args.output,
sensor_model=args.sensor_model,
hardware_revisions=("*",),
sealing=sealing,
)
except ThreeDError as exc:
print(f"rejected: {exc.diagnostic.code}: {exc.diagnostic.message}", file=sys.stderr)
return 2
envelope = describe_envelope(report.output)
summary = {
"status": "ok",
"output": str(report.output),
"asset_hash": report.asset_hash,
"vertices": report.part_vertex_counts,
"triangles": report.part_triangle_counts,
"geometry_id": assets.geometry_id,
"field_domain_id": assets.field_domain_id,
"source_geometry_id": assets.source_geometry_id,
"envelope": envelope,
"platform_lock": (
assets.platform_lock.to_dict() if assets.platform_lock is not None else None
),
}
if args.json:
print(json.dumps(summary, indent=2, ensure_ascii=False))
else:
print(
f"wrote {report.output} vertices={report.part_vertex_counts} "
f"triangles={report.part_triangle_counts}"
)
print(f" geometry_id={assets.geometry_id}")
print(
" source_geometry_id="
+ (assets.source_geometry_id or "(none: no world_xyz in the map)")
)
print(f" asset_hash={report.asset_hash}")
print(
f" envelope=v{envelope['envelope_version']} {envelope['payload']}"
f"{' signed(' + str(envelope['signing_key']) + ')' if envelope['signed'] else ' unsigned'}"
+ (f" sealed for key {envelope['key_id']}" if envelope["sealed"] else "")
)
if summary["platform_lock"]:
print(f" platform_lock={json.dumps(summary['platform_lock'], ensure_ascii=False)}")
return 0
if __name__ == "__main__":
sys.exit(main())