三维显示骨架:可执行合同 + 模块签名 + 分阶段计划 - What: 按 docs/architecture/studio-3d-design.md 与 studio-3d-contracts.pyi 搭建骨架(阶段 0)。shared/src/orisys_app_shared/scene3d/:contracts.py 把 .pyi 的规格变成可执行代码——全部 DTO/枚举(FrameIdentity/FieldDescriptor/SurfaceFieldFrame/MeshPart/SurfaceBinding/CameraPreset/ExpectedGeometry/SceneAssets/ColorScale/DisplaySettings/VertexFields/RenderFrame/CameraMatrices/GLCapabilities/RenderStats/CompileOptions/CompileReport)、标准诊断码表(DIAGNOSTIC_SEVERITY,含 stale_frame=info、field_unavailable=warning 等)、ThreeDError、freeze()(拥有数据 + C 连续 + 只读)、五个校验器(validate_surface_frame/validate_binding/validate_mesh_part/validate_assets/validate_display_settings/validate_render_frame)、LatestFrameMailbox(容量 1、替换计数、锁内只换引用)、UNAVAILABLE_DESCRIPTOR/rest_descriptor() 与 summarize_parts(),以及全部 Protocol;该模块不 import Qt/OpenGL/SDK。assets.py/sampling.py/composer.py 为签名骨架(未实现处抛 NotImplementedError 并写明归属阶段)。apps/studio/src/orisys_studio/scene3d/:camera/adapters/coordinator/gl_backend/widget/panel 签名骨架,其中 configure_surface_format() 已实现(QApplication 之前调用,desktop33=GL3.3 Core / gles30=GLES3.0,深度 24、无 MSAA);模块级一律不 import GL 绑定,PyOpenGL 只在 GLDispatch.bind_current_context 里加载。tools/scene_assets/{probe_gl,validate_scene,compile_scene}.py 提供入口与参数,未实现时输出 not_implemented 并返回 2(避免被 CI 当成通过)。docs/architecture/studio-3d-stages.md:阶段 0-5 的产出、每阶段的验收矩阵、并行度与回归门。测试:shared/tests/test_scene3d_contracts.py 26 项(freeze 拥有数据、可写/非连续数组被拒、scalar 与 valid 形状一致、位移缺 quality 被拒、非有限 scalar 标为 valid 只报警不静默、绑定越界/-1 约定/权重和为 1、三角形索引越界、感知法线单位长度、恰好一个 sensing part、binding 必须属于 sensing part、设备几何/域/尺寸不符被拒、色标 lower<upper、邮箱替换计数与多线程不阻塞生产者、身份区分 source/generation、骨架可导入且不引入 PyOpenGL);apps/studio/tests/test_scene3d_skeleton.py 4 项(骨架不 import OpenGL、surface format 两个 profile、widget 构造不需要 context、各 stub 报出归属阶段)。 - Why: 用户要求"先从骨架搭起,然后计划功能实现顺序,最后逐阶段实现与验收"。骨架阶段的价值在于把接口变成可执行的(.pyi 本身无法被测试),并把"数组只读、单位与质量必须声明、近似位移不得当毫米、参考帧不得冒充测量、显示丢帧不得计入记录丢帧"这些容易在实现时被绕过的规则先固定下来;同时明确不 import Qt/OpenGL 的边界,使采集线程与 CI 能在无 GPU 环境导入它。 - Verification: shared 全量 187 passed(原 161 + 26);studio 95 passed(原 91 + 4);ui_check 554 项检查 0 失败;compileall 通过;probe_gl --profile desktop33 在未实现时返回 2 并说明将产出什么报告。 - Not in this change: 三维功能本身(阶段 1 起);app_window/main.py 未接线,骨架对现有应用完全惰性(无任何模块被应用导入)。

This commit is contained in:
ykcheungab
2026-09-14 14:32:38 +08:00
parent c10e579371
commit 0b309e7edf
18 changed files with 2327 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
"""Compile a validated scene, and optionally a housing GLB, into a runtime package.
Stage 2 and stage 5 deliverables (``docs/architecture/studio-3d-stages.md``).
Offline only: this tool is what turns a Blender export and the legacy NPZ geometry
into the ``.oryscene`` payload the runtime loads. It bakes the coordinate
transform, the sampling table and the LOD once, so the runtime never repeats that
work; it signs with a key from the build environment, so no signing key ever
reaches a customer runtime.
Until stage 2 lands, no package is produced and the exit code is non-zero, so a
release script cannot ship a package that was never compiled.
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
_STAGE = "stage 2 (asset contracts) and stage 5 (release protection)"
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--legacy-dir", type=Path, default=None, help="legacy NPZ export")
parser.add_argument("--map", type=Path, default=None, help="run-time UV mapping (npz)")
parser.add_argument("--housing", type=Path, default=None, help="housing GLB")
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="0.0.0")
args = parser.parse_args(argv)
print(
"not_implemented: "
f"{_STAGE}. Would write {args.output} from "
f"legacy={args.legacy_dir} map={args.map} housing={args.housing} "
f"as {args.asset_id} {args.version}."
)
return 2
if __name__ == "__main__":
sys.exit(main())
+65
View File
@@ -0,0 +1,65 @@
"""Probe the real GL stack from inside a Qt window and write a diagnostic report.
Stage 1 deliverable (``docs/architecture/studio-3d-stages.md``).
It must create an **actual** Qt widget and read the context that Qt actually
obtained — asking the system whether ``libGL`` exists proves nothing, and an
offscreen UI test cannot stand in for a board's GPU. What it writes is the input
to the platform decision: which profile a board gets, and whether the renderer is
a real GPU.
Until stage 1 lands, the script explains what it will do and exits non-zero, so a
build script cannot mistake "not implemented" for "probe passed".
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
_STAGE = "stage 1 (platform PoC)"
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument(
"--profile",
choices=("desktop33", "gles30"),
default="desktop33",
help="the start-up profile to probe (one per process)",
)
parser.add_argument(
"--output",
type=Path,
default=None,
help="write the diagnostic report here as JSON",
)
parser.add_argument(
"--windowed",
action="store_true",
help="create a visible window instead of a short-lived one",
)
args = parser.parse_args(argv)
report = {
"status": "not_implemented",
"profile": args.profile,
"stage": _STAGE,
"note": (
"The probe creates a real QOpenGLWidget and reports the context Qt "
"obtained: vendor, renderer, GL/GLSL version, DPR, framebuffer size, "
"and whether the renderer is a software rasteriser. It never reports "
"a board as verified from an offscreen or virtual context."
),
}
text = json.dumps(report, indent=2, ensure_ascii=False)
if args.output is not None:
args.output.write_text(text + "\n", encoding="utf-8")
print(text)
return 2
if __name__ == "__main__":
sys.exit(main())
+45
View File
@@ -0,0 +1,45 @@
"""Validate a scene package (or a legacy import) without a GPU.
Stage 2 deliverable (``docs/architecture/studio-3d-stages.md``): the checks the
loader and the compiler must agree on, runnable on a build machine and in CI.
Until stage 2 lands, importing an asset is not implemented; the argument parsing
and the exit contract exist so the CI job can be wired now.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
_STAGE = "stage 2 (asset contracts)"
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("asset", type=Path, help="scene package or legacy asset directory")
parser.add_argument("--housing", type=Path, default=None, help="housing GLB to check")
parser.add_argument("--json", action="store_true", help="machine-readable output")
args = parser.parse_args(argv)
report = {
"status": "not_implemented",
"asset": str(args.asset),
"stage": _STAGE,
"note": (
"Checks: manifest/schema, signature and entry hashes, coordinate frame "
"and units, part roles, index ranges, binding weights and domain, "
"geometry/domain match against the device description."
),
}
if args.json:
print(json.dumps(report, indent=2, ensure_ascii=False))
else:
print(f"{report['status']}: {report['note']}")
return 2
if __name__ == "__main__":
sys.exit(main())