Files
orisys-app/shared/tests/test_scene3d_live.py
T
ykcheungab ef487098c9 feat(studio-3d): 限速、真实耗时统计、无位移复用资产几何,以及保护包密钥注入(阶段 9 上半)
- What:
  1. **显示限速真的生效**(R5):`ThreeDCoordinator.tick()` 用注入的单调时钟与
     `display_period_s() = 1/max_fps` 限制**合成启动**频率(窗口定时器仍是 16 ms,但"显示帧率"是给操作者的
     承诺,不是定时器的描述);声明边界(activate/begin_session/show_rest/隐藏)把预算清零,所以按下开始后的
     第一帧立刻合成。生产者侧改为**按需**跳过:`Scene3DView.submit_packet()` 在
     `coordinator.has_pending_frame()`(mailbox 里已有一帧等待)时不再构造快照——那是 O(H×W) 的拷贝,
     而 mailbox 只保留最新一帧,两次拷贝里必然丢一个;被跳过的数量计入新的 `skipped_producer`,
     与"显示跳帧"分开。**不丢任何本该显示的帧**(等待中的那一帧仍在)。
  2. **真实统计**:`RenderFrame.compose_ms`(composer 在构造帧之前测量并写入,不再读会被下一任务改写的
     `last_stats`);`GLRenderer` 的绘制计数改名为 `paint_calls`,`RenderStats.displayed_frames` 由
     **widget 按帧身份**统计(相机旋转/色标改动只是重画,不再是"多显示了一帧");`render_totals()` 增
     `compose_ms`/`latency_ms`(合成开始→被接受的单调耗时)/`paint_calls`/`skipped_producer`,并且
     `shown`/`compose_ms` 改从 widget 读(无 GL 的机器也能报告,之前只有画过才有数)。
  3. **无位移时复用资产几何**(R5 建议):`SceneComposer` 缓存 `asset_hash → (positions, normals)`;
     变形时照旧从形变后的位置重算法线,不变形时直接用资产自己的(只读、零拷贝)数组。20k 顶点实测:
     compose p50 **6.15 ms → 1.03 ms**(法线段 4.9 ms → 0.0001 ms,剩余是采样 1.01 ms);
     真实发布包(1032 顶点)compose p50 **0.50 ms → 0.17 ms**。
  4. **保护包密钥注入**(R4):新增 `scene3d/keys.py`——从 preference 读取
     `scene3d_verify_key`(验签公钥 PEM)/`scene3d_content_key_file` + `scene3d_content_key_id`
     (内容密钥,经 `protect.load_key_file`)/`scene3d_require_signature`(发布模式,拒绝无签名包),
     组装成 `SceneKeys` 交给 `SceneAssetLoader` → `load_package` → `PackagedAssetLoader.load(keys=…,
     verify_key_pem=…)`;`AppWindow._sync_3d_source()` 解析它并 `Mesh3dPanel.set_keys()`。
     缺失/不可读的密钥是**状态而不是崩溃**(`describe()` 里带原因),密钥材料永不进日志;
     `Mesh3dPanel.describe()` 的 scene 块记录 `keys`(布尔 + 路径)。
- Why: 审查报告 R5(`max_fps` 只被存储与显示、`draw()` 硬编码 `compose_ms=0.0`、`_displayed_frames` 把
  重画也算新帧、`SceneComposer.last_stats` 被显示链路完全未读)与 R4(底层支持签名/加密包,Studio 加载
  入口却从不传验签公钥与 KeyProvider,合法保护包打不开,只能发明文包或关掉校验)。R5 的另一半是
  "当前只有曲面着色、没有 displacement 时,composer 仍每帧重算法线并重新打包位置"——缓存后 20k 顶点
  降低到六分之一。
- Verification:
  · `test_scene3d_coordinator.py` +6:15 fps 的假时钟下 1.32 s 内只启动约 20 次合成(60 fps 对照 ≥55 次)、
  `display_period_s()` 跟随设置、结果延迟是真实单调差值、`has_pending_frame()` 随 mailbox 变化;
  `test_scene3d_live.py` +3:无位移时发布帧直接**就是**资产自己的(只读)数组、force_deform 但仍无位移时
  给出 `field_unavailable` 诊断、有标定位移时仍重算、`compose_ms` 属于该帧且不被下一次合成改写;
  `test_scene3d_view.py` +2:等待中的帧让生产者跳过第二个快照且取出后立即恢复、`render_totals()` 的
  compose 成本与延迟是真实值;新增 `test_scene3d_protected_package.py` 7 项:Studio 入口打开签名包、
  用 preference 里的内容密钥打开加密包、缺验签公钥 → `asset_auth_failed`、密钥文件缺失/无 key id →
  状态而非崩溃、发布模式拒绝无签名包而开发模式接受、`SceneAssetLoader` 把密钥交给 worker、面板携带密钥。
  · 全量:shared+apps 同一次运行 **588 项 0 失败 15 跳过**;真 GL 冒烟(`QT_QPA_PLATFORM=windows`)
  **14 项 0 失败**;ui_check 587 项 0 失败;locale_check 0 缺失。
  · 顺带修掉两个**既有的顺序依赖**真 GL 用例(不是本次引入:在上一提交上单跑同样失败):
  `test_the_producer_side_never_waits_for_the_gpu` 的收尾没有排空 mailbox(留下的一帧会被下一个测试
  上传),`test_an_automatic_colour_scale_follows_the_frame` 在渲染之前就断言"范围来自本帧",
  且用"不同颜色数"当自动量程的代理(对可见面积敏感)。现在前者排空并断言排空完成,后者先渲染再断言,
  判据改为**平均亮度**(固定 0..1000 把 3..7 压进 LUT 最暗的一小段,自动量程铺满整条色带)。
  · 性能报告(`.repro/perf-cpu-*-after-9.json`,`bench_scene.py --cpu-only`):合成 p50
  合成 poc 0.016 ms / 20k 顶点 1.03 ms / 真实包 0.17 ms;对比已提交的 `perf-report-windows-dev.json`
  (20k 6.15 ms、真实包 0.50 ms)。
- Not in this change: **画布布局第二批**(2D/3D 页签容器、参数抽屉、窄窗口回退、诊断抽屉)与曲线量程开关
  仍未做;阶段 9 的门禁部分(`soak_scene.py --minutes 120` GUI 模式、Jetson Orin Nano 与 QCS6490 上的
  `probe_gl/bench_scene/soak_scene` 报告)需要长时间运行与板卡,本轮未跑,结论不写"通过";
  签名私钥仍只在构建机(本提交只引入"读"的一半)。
2026-09-14 21:52:02 +08:00

465 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Stage 3 acceptance: sampling, composition, and the frame schedule.
The three rows that matter most are the ones that are easy to get subtly wrong and
impossible to notice on screen: an invalid tap must invalidate the *vertex* rather
than read as zero, the display gain must not touch the measurement, and a producer
must never be able to make the display queue grow.
"""
from __future__ import annotations
import threading
import time
from concurrent.futures import ThreadPoolExecutor
import numpy as np
import pytest
from orisys_app_shared.scene3d import synthetic
from orisys_app_shared.scene3d.composer import DISPLAY_ILLUSTRATIVE, SceneComposer
from orisys_app_shared.scene3d.contracts import (
ColorScale,
DisplaySettings,
FieldDescriptor,
FrameIdentity,
Quality,
RenderFrame,
SurfaceFieldFrame,
ThreeDError,
freeze,
rest_descriptor,
)
from orisys_app_shared.scene3d.sampling import (
SurfaceSampler,
remap_to_domain,
sample_field,
)
# --------------------------------------------------------------------- helpers
def _frame(scene, scalar, valid=None, *, displacement=None, quality=None,
identity=None, domain=None, shape=None):
scalar = np.asarray(scalar, dtype=np.float32)
if valid is None:
valid = np.ones(scalar.shape, dtype=bool)
frame = SurfaceFieldFrame(
identity=identity or FrameIdentity("live", 1, 1, 0.1, generation=1),
geometry_id=scene.geometry_id,
field_domain_id=domain or scene.field_domain_id,
scalar=freeze(scalar, dtype=np.float32),
scalar_descriptor=FieldDescriptor("flow_magnitude", "px", "approximate", False),
valid=freeze(np.asarray(valid, dtype=bool), dtype=np.bool_),
displacement_sensor_m=(
freeze(np.asarray(displacement, dtype=np.float32), dtype=np.float32)
if displacement is not None
else None
),
displacement_quality=quality,
)
if shape is not None:
object.__setattr__(frame, "scalar", freeze(np.asarray(shape, dtype=np.float32), dtype=np.float32))
return frame
def _binding(shape=(3, 3), indices=None, weights=None):
"""The identity binding of the PoC scene, optionally overridden."""
scene = synthetic.poc_scene()
binding = scene.binding
if indices is None and weights is None and shape == (3, 3):
return binding
from orisys_app_shared.scene3d.contracts import SurfaceBinding
count = int(indices.shape[0]) if indices is not None else shape[0] * shape[1]
return SurfaceBinding(
part_id="sensing_surface",
geometry_id=scene.geometry_id,
field_domain_id=scene.field_domain_id,
shape_hw=shape,
sample_indices=freeze(np.asarray(indices, dtype=np.int32), dtype=np.int32),
sample_weights=freeze(np.asarray(weights, dtype=np.float32), dtype=np.float32),
)
# --------------------------------------------------------------------- sampling
def test_a_constant_field_samples_to_the_same_constant():
scene = synthetic.poc_scene()
fields = sample_field(_frame(scene, np.full((3, 3), 7.5)), scene.binding)
assert np.allclose(np.asarray(fields.scalar), 7.5)
assert np.asarray(fields.valid).all()
def test_a_ramp_field_samples_exactly_like_the_reference():
"""Every vertex samples its own cell, so the sampled value is the cell value."""
scene = synthetic.poc_scene()
ramp = np.arange(9, dtype=np.float32).reshape(3, 3)
fields = sample_field(_frame(scene, ramp), scene.binding)
assert np.array_equal(np.asarray(fields.scalar), ramp.reshape(-1))
def test_a_single_sample_spike_reaches_exactly_one_vertex():
scene = synthetic.poc_scene()
field = np.zeros((3, 3), dtype=np.float32)
field[1, 1] = 5.0
fields = sample_field(_frame(scene, field), scene.binding)
scalar = np.asarray(fields.scalar)
assert int((scalar > 0).sum()) == 1
assert scalar[4] == 5.0
def test_bilinear_sampling_averages_the_four_taps():
"""A vertex between pixels gets the weighted average, computed by hand."""
scene = synthetic.poc_scene()
indices = np.array([[0, 1, 3, 4]], dtype=np.int32)
weights = np.array([[0.25, 0.25, 0.25, 0.25]], dtype=np.float32)
binding = _binding(indices=indices, weights=weights)
field = np.array([[1.0, 2.0, 0.0], [3.0, 4.0, 0.0], [0.0, 0.0, 0.0]], dtype=np.float32)
fields = sample_field(_frame(scene, field), binding)
assert np.asarray(fields.scalar)[0] == pytest.approx((1 + 2 + 3 + 4) / 4)
assert bool(np.asarray(fields.valid)[0])
def test_one_invalid_tap_invalidates_the_vertex_and_is_not_read_as_zero():
"""A gap is not a measurement of zero: the value must be absent."""
scene = synthetic.poc_scene()
indices = np.array([[0, 1, 3, 4]], dtype=np.int32)
weights = np.array([[0.25, 0.25, 0.25, 0.25]], dtype=np.float32)
binding = _binding(indices=indices, weights=weights)
field = np.array([[1.0, 2.0, 0.0], [3.0, 4.0, 0.0], [0.0, 0.0, 0.0]], dtype=np.float32)
valid = np.ones((3, 3), dtype=bool)
valid[1, 1] = False # index 4 is a positive-weight tap
fields = sample_field(_frame(scene, field, valid), binding)
assert not bool(np.asarray(fields.valid)[0])
assert np.isnan(np.asarray(fields.scalar)[0]), (
"an invalid vertex must carry no value at all, not a sum with a zero in it"
)
def test_an_unused_tap_is_neither_a_contribution_nor_a_reason_to_invalidate():
scene = synthetic.poc_scene()
indices = np.array([[4, -1, -1, -1]], dtype=np.int32)
weights = np.array([[1.0, 0.0, 0.0, 0.0]], dtype=np.float32)
binding = _binding(indices=indices, weights=weights)
field = np.zeros((3, 3), dtype=np.float32)
field[1, 1] = 3.0
valid = np.ones((3, 3), dtype=bool)
valid[0, 0] = False # unused, so it must not matter
fields = sample_field(_frame(scene, field, valid), binding)
assert bool(np.asarray(fields.valid)[0])
assert np.asarray(fields.scalar)[0] == pytest.approx(3.0)
def test_a_non_finite_scalar_invalidates_rather_than_colouring_nan():
scene = synthetic.poc_scene()
field = np.full((3, 3), 1.0, dtype=np.float32)
field[2, 2] = np.nan
fields = sample_field(_frame(scene, field), scene.binding)
assert not bool(np.asarray(fields.valid)[8])
assert np.isnan(np.asarray(fields.scalar)[8])
def test_a_zero_measurement_is_valid_and_stays_zero():
"""Zero is a value. Only `valid` decides whether it means anything."""
scene = synthetic.poc_scene()
fields = sample_field(_frame(scene, np.zeros((3, 3), dtype=np.float32)), scene.binding)
assert np.asarray(fields.valid).all()
assert np.all(np.asarray(fields.scalar) == 0.0)
assert not np.isnan(np.asarray(fields.scalar)).any()
def test_the_sampler_refuses_a_frame_from_another_domain():
scene = synthetic.poc_scene()
frame = _frame(scene, np.zeros((3, 3), dtype=np.float32), domain="some-other-domain")
with pytest.raises(ThreeDError) as excinfo:
sample_field(frame, scene.binding)
assert excinfo.value.diagnostic.code == "field_domain_mismatch"
def test_the_sampler_refuses_a_frame_of_the_wrong_shape():
scene = synthetic.poc_scene()
frame = _frame(scene, np.zeros((4, 4), dtype=np.float32), shape=np.zeros((4, 4), dtype=np.float32))
with pytest.raises(ThreeDError) as excinfo:
sample_field(frame, scene.binding)
assert "shape does not match" in excinfo.value.diagnostic.message
def test_sampling_is_vectorised_enough_for_a_large_mesh():
"""A full-resolution binding is ~10^5 vertices; sampling must not crawl."""
scene = synthetic.poc_scene()
count = 20_000
rng = np.random.default_rng(7)
indices = rng.integers(0, 9, size=(count, 4)).astype(np.int32)
weights = rng.random((count, 4)).astype(np.float32)
weights /= weights.sum(axis=1, keepdims=True)
binding = _binding(indices=indices, weights=weights)
frame = _frame(scene, rng.random((3, 3)).astype(np.float32))
started = time.perf_counter()
fields = sample_field(frame, binding)
elapsed = (time.perf_counter() - started) * 1000.0
assert np.asarray(fields.scalar).shape == (count,)
assert elapsed < 150.0, f"sampling {count} vertices took {elapsed:.1f} ms"
# ----------------------------------------------------------------------- remap
def test_a_remap_gathers_the_source_pixels_the_map_names():
field = np.arange(12, dtype=np.float32).reshape(3, 4)
map_x = np.array([[0, 1], [2, 3]])
map_y = np.array([[0, 0], [2, 2]])
values, valid = remap_to_domain(field, map_x=map_x, map_y=map_y, shape_hw=(2, 2))
assert np.array_equal(np.asarray(values), [[0.0, 1.0], [10.0, 11.0]])
assert np.asarray(valid).all()
def test_a_remap_that_has_no_source_pixel_leaves_a_hole_not_a_zero():
field = np.full((3, 3), 5.0, dtype=np.float32)
map_x = np.array([[0, -1], [1, 1]])
map_y = np.array([[0, -1], [1, 1]])
values, valid = remap_to_domain(field, map_x=map_x, map_y=map_y, shape_hw=(2, 2))
assert np.asarray(valid).tolist() == [[True, False], [True, True]]
assert np.isnan(np.asarray(values)[0, 1]), "a hole must not read as zero"
assert np.asarray(values)[0, 0] == 5.0
def test_a_remap_keeps_a_trailing_axis_for_vector_fields():
flow = np.zeros((3, 3, 2), dtype=np.float32)
flow[..., 0] = 1.0
flow[..., 1] = 2.0
map_x = np.array([[0, 1]])
map_y = np.array([[1, 2]])
values, valid = remap_to_domain(flow, map_x=map_x, map_y=map_y, shape_hw=(1, 2))
assert np.asarray(values).shape == (1, 2, 2)
assert np.asarray(values)[0, 0].tolist() == [1.0, 2.0]
assert np.asarray(valid).all()
def test_a_remap_of_the_wrong_shape_is_refused():
field = np.zeros((3, 3), dtype=np.float32)
with pytest.raises(ThreeDError) as excinfo:
remap_to_domain(
field, map_x=np.zeros((2, 2)), map_y=np.zeros((2, 2)), shape_hw=(3, 3)
)
assert excinfo.value.diagnostic.code == "field_domain_mismatch"
def test_a_remap_leaves_the_result_read_only():
field = np.zeros((2, 2), dtype=np.float32)
values, valid = remap_to_domain(
field, map_x=np.zeros((2, 2)), map_y=np.zeros((2, 2)), shape_hw=(2, 2)
)
assert not np.asarray(values).flags["WRITEABLE"]
assert not np.asarray(valid).flags["WRITEABLE"]
# ------------------------------------------------------------------- composition
def test_the_gain_moves_positions_and_never_the_measurement():
"""The row the whole colour scale depends on."""
scene = synthetic.poc_scene()
count = 9
field = np.linspace(0.0, 1.0, count, dtype=np.float32).reshape(3, 3)
displacement = np.zeros((3, 3, 3), dtype=np.float32)
displacement[:, :, 2] = 0.001 # a millimetre of illustrative relief
frame = _frame(scene, field, displacement=displacement, quality="approximate")
composer = SceneComposer()
quiet = composer.compose(scene, frame, DisplaySettings(deform_enabled=False))
small = composer.compose(
scene, frame, DisplaySettings(deform_enabled=True, display_gain=1.0)
)
large = composer.compose(
scene, frame, DisplaySettings(deform_enabled=True, display_gain=50.0)
)
# The measurement is identical in all three.
assert np.array_equal(np.asarray(quiet.scalar), np.asarray(small.scalar))
assert np.array_equal(np.asarray(small.scalar), np.asarray(large.scalar))
assert np.array_equal(np.asarray(quiet.valid), np.asarray(large.valid))
# The positions are not.
rest = np.asarray(small.positions_sensor_m)
moved = np.asarray(large.positions_sensor_m)
rest_scene = np.asarray(scene.sensing_part().positions_sensor_m)
assert np.allclose((rest - rest_scene)[:, :2], 0.0, atol=1e-7), "the gain moved sideways"
assert np.allclose((rest - rest_scene)[:, 2], 0.001, atol=1e-7)
assert np.allclose((moved - rest_scene)[:, 2], 0.05, atol=1e-7)
def test_an_illustrative_deformation_says_so():
scene = synthetic.poc_scene()
displacement = np.zeros((3, 3, 3), dtype=np.float32)
displacement[:, :, 2] = 0.001
frame = _frame(scene, np.zeros((3, 3), dtype=np.float32), displacement=displacement,
quality="approximate")
composed = SceneComposer().compose(
scene, frame, DisplaySettings(deform_enabled=True, display_gain=3.0)
)
codes = [item.code for item in composed.diagnostics]
assert DISPLAY_ILLUSTRATIVE in codes, codes
note = next(item for item in composed.diagnostics if item.code == DISPLAY_ILLUSTRATIVE)
assert note.severity == "info"
assert "illustration" in note.message and "×3" in note.message
def test_a_calibrated_displacement_does_not_need_the_caveat():
scene = synthetic.poc_scene()
displacement = np.zeros((3, 3, 3), dtype=np.float32)
displacement[:, :, 2] = 0.0005
frame = _frame(scene, np.zeros((3, 3), dtype=np.float32), displacement=displacement,
quality="calibrated")
composed = SceneComposer().compose(
scene, frame, DisplaySettings(deform_enabled=True, display_gain=2.0)
)
assert DISPLAY_ILLUSTRATIVE not in [item.code for item in composed.diagnostics]
def test_deformation_without_a_displacement_colours_in_place_and_says_so():
scene = synthetic.poc_scene()
frame = _frame(scene, np.full((3, 3), 2.0, dtype=np.float32))
composed = SceneComposer().compose(
scene, frame, DisplaySettings(deform_enabled=True, display_gain=10.0)
)
assert np.allclose(
np.asarray(composed.positions_sensor_m),
np.asarray(scene.sensing_part().positions_sensor_m),
)
assert any(item.code == "field_unavailable" for item in composed.diagnostics)
def test_normals_are_recomputed_from_the_deformed_positions():
"""A bump must change the shading on its slopes."""
scene = synthetic.poc_scene()
displacement = np.zeros((3, 3, 3), dtype=np.float32)
# Only the centre vertex is lifted: the slopes around it have to tilt. (The
# apex itself stays vertical, which is the correct normal for a symmetric
# peak — asserting otherwise would be asserting a bug.)
displacement[1, 1, 2] = 0.004
frame = _frame(scene, np.zeros((3, 3), dtype=np.float32), displacement=displacement,
quality="approximate")
composed = SceneComposer().compose(
scene, frame, DisplaySettings(deform_enabled=True, display_gain=1.0)
)
normals = np.asarray(composed.normals_sensor)
assert np.allclose(np.linalg.norm(normals, axis=1), 1.0, atol=1e-5)
rest_normals = np.asarray(scene.sensing_part().normals_sensor)
assert not np.allclose(normals, rest_normals), "the normals ignored the deformation"
# Only the vertices whose triangles touch the lifted one change; the two
# diagonal corners (2 and 6) share no triangle with it and correctly stay flat.
neighbours = [0, 1, 3, 5, 7, 8]
untouched = [2, 6]
tilted = np.array([float(normals[index, 2]) for index in neighbours])
assert (tilted < 0.999).all(), f"the slopes did not tilt: {tilted}"
flat = np.array([float(normals[index, 2]) for index in untouched])
assert np.allclose(flat, 1.0, atol=1e-6), flat
assert normals[4, 2] == pytest.approx(1.0, abs=1e-3), (
"the apex of a symmetric bump is vertical"
)
# ...and the reverse bump tilts the other way.
upside_down = -displacement
flipped = SceneComposer().compose(
scene,
_frame(scene, np.zeros((3, 3), dtype=np.float32), displacement=upside_down,
quality="approximate"),
DisplaySettings(deform_enabled=True, display_gain=1.0),
)
flipped_normals = np.asarray(flipped.normals_sensor)
assert np.allclose(
flipped_normals[neighbours, 0], -normals[neighbours, 0], atol=1e-6
), "the reverse bump must tilt the slopes the other way"
def test_the_composer_refuses_a_frame_from_another_geometry():
scene = synthetic.poc_scene()
frame = _frame(scene, np.zeros((3, 3), dtype=np.float32))
object.__setattr__(frame, "geometry_id", "someone-elses")
with pytest.raises(ThreeDError) as excinfo:
SceneComposer().compose(scene, frame, DisplaySettings())
assert excinfo.value.diagnostic.code == "geometry_mismatch"
def test_a_reference_frame_measures_nothing():
scene = synthetic.poc_scene()
rest = SceneComposer().rest_frame(scene, FrameIdentity("live", 1, 0, 0.0, 1))
assert not np.asarray(rest.valid).any()
assert np.isnan(np.asarray(rest.scalar)).all()
assert rest.scalar_descriptor.quantity == "unavailable"
assert rest.scalar_descriptor.quality == "display_only"
assert np.allclose(
np.asarray(rest.positions_sensor_m),
np.asarray(scene.sensing_part().positions_sensor_m),
)
def test_the_composed_frame_carries_read_only_arrays():
scene = synthetic.poc_scene()
composed = SceneComposer().compose(
scene, _frame(scene, np.zeros((3, 3), dtype=np.float32)), DisplaySettings()
)
for name in ("positions_sensor_m", "normals_sensor", "scalar", "valid"):
value = getattr(composed, name)
assert not value.flags["WRITEABLE"], f"{name} is writable"
assert value.flags["C_CONTIGUOUS"]
def test_the_composer_does_not_rebuild_the_binding_or_the_indices():
"""Composition must be cheap enough to run per frame."""
scene = synthetic.poc_scene()
composer = SceneComposer()
frame = _frame(scene, np.zeros((3, 3), dtype=np.float32))
for _ in range(3):
composer.compose(scene, frame, DisplaySettings())
assert composer.last_stats.total_ms < 50.0, composer.last_stats
# --------------------------------------------- stage 9: reuse and real numbers
def test_colouring_in_place_reuses_the_assets_own_geometry():
"""No displacement to draw means the picture *is* the asset: nothing to recompute.
Normals were recomputed from the same untouched positions on every frame, and the
measured cost was the bulk of a 20k-vertex compose. The reused arrays are the
asset's own (frozen) ones, so nothing is copied either.
"""
scene = synthetic.poc_scene()
part = scene.sensing_part()
composer = SceneComposer()
frame = _frame(scene, np.zeros((3, 3), dtype=np.float32))
composed = composer.compose(scene, frame, DisplaySettings())
assert composed.positions_sensor_m is part.positions_sensor_m
assert composed.normals_sensor is part.normals_sensor
# A deform request with nothing to deform is the same case, and says so.
composed = composer.compose(
scene, frame, DisplaySettings(deform_enabled=True, display_gain=5.0)
)
assert composed.positions_sensor_m is part.positions_sensor_m
assert any(item.code == "field_unavailable" for item in composed.diagnostics)
def test_a_calibrated_displacement_still_recomputes_positions_and_normals():
"""The cache is for the colour-in-place case only, never for a deformed surface."""
scene = synthetic.poc_scene()
part = scene.sensing_part()
displacement = np.zeros((3, 3, 3), dtype=np.float32)
displacement[1, 1, 2] = 0.5
frame = _frame(
scene,
np.ones((3, 3), dtype=np.float32),
displacement=displacement,
quality="calibrated",
)
composed = SceneComposer().compose(
scene, frame, DisplaySettings(deform_enabled=True, display_gain=2.0)
)
assert composed.positions_sensor_m is not part.positions_sensor_m
assert composed.normals_sensor is not part.normals_sensor
assert not np.allclose(
np.asarray(composed.positions_sensor_m), np.asarray(part.positions_sensor_m)
)
def test_the_composed_frame_carries_its_own_cost():
"""The panel's "compose ms" reads this, not the composer's shared counter."""
scene = synthetic.poc_scene()
composer = SceneComposer()
frame = _frame(scene, np.zeros((3, 3), dtype=np.float32))
composed = composer.compose(scene, frame, DisplaySettings())
assert composed.compose_ms > 0.0
assert composed.compose_ms == pytest.approx(composer.last_stats.total_ms)
# A second compose rewrites ``last_stats``; the first frame's number must not move.
first = composed.compose_ms
composer.compose(scene, frame, DisplaySettings())
assert composed.compose_ms == first