Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ jobs:
QT_QPA_PLATFORM: offscreen
NUMBA_DISABLE_JIT: "1"
run: >-
python -m pytest -q
python -m pytest -q -m "not windows_compositor"
tests/gui/test_detail_pipeline.py
tests/gui/test_detail_render_coordinator.py
tests/gui/test_detail_decode_backend.py
Expand All @@ -264,7 +264,10 @@ jobs:
tests/gui/test_detail_surface_residency.py
tests/gui/test_detail_render_session.py
tests/ui/controllers/test_player_view_controller_adjustments.py
tests/ui/controllers/test_player_view_init_cover.py
tests/ui/widgets/test_gl_image_viewer_post_load_signal.py
tests/ui/widgets/test_still_texture_residency.py
tests/ui/widgets/test_video_area.py
tests/test_detail_benchmark.py
tests/test_detail_surface_cache_benchmark.py

Expand Down
47 changes: 45 additions & 2 deletions docs/WINDOWS_SCAN_PLAYBACK_DIAGNOSTICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,51 @@ failed graphics contract, not a successful reproduction run.

The extra active-surface `frameSubmitted` is a QRhi submission heuristic, not a
DXGI/DWM presentation fence. It verifies the application state machine and an
additional Qt composition submission; only the real Windows repetition matrix
can determine whether it fixes the user-visible first-frame leak.
additional Qt composition submission. The reveal wait is bounded: after the
matching current-generation media submission, a missing extra composition may
end with `post_submit_deadline` followed by `surface_revealed` with
`reason=deadline`. A stale transition must instead record
`post_submit_discarded` and must not reveal its media. Only the real Windows
repetition matrix can determine whether this preserves the user-visible
first-frame leak fix.

Transition traces must show `presentation_suppressed` before the exposed
surface's `video_surface_blank_requested`/blank submission and
`presentation_resumed` only after matching new content is installed. A rapid
A→B switch must record `post_submit_discarded`
for any scheduled or armed A barrier, including `reason`, `state`, and the old
epoch. During the suppressed interval a real compositor screenshot must contain
the opaque Detail background, never A's pixels or media-specific overlays.
When async preparation changes the active video renderer, the trace must add a
second `video_surface_blank_requested` with `reason=surface_switch`, the current
Detail generation, the current media generation, and `surface=adjusted|native`.
Its matching `presentation_resumed` must follow successful frame installation.

Run the real-pixel contracts manually from a normal interactive Windows
desktop. Setting the platform explicitly is required because the unit-test
fixtures otherwise default to `offscreen`:

```powershell
$env:QT_QPA_PLATFORM = "windows"
$env:IPHOTO_RHI_BACKEND = "opengl"
$env:IPHOTO_WINDOWS_COMPOSITOR_CYCLES = "100"
$env:IPHOTO_WINDOWS_COMPOSITOR_FORCE_UPLOAD_FAILURE = "1"
$tests = @(
"tests/ui/widgets/test_gl_image_viewer_post_load_signal.py",
"tests/ui/widgets/test_video_area.py"
)
1..30 | ForEach-Object {
python -m pytest -q -m windows_compositor $tests
if ($LASTEXITCODE -ne 0) { throw "Compositor run $_ failed" }
}
```

The video pixel contract drives `VideoArea.begin_load()` followed by
`PlayerViewController.begin_video_transition()`; it must not request the blank
frame directly from the tested QRhi child.
Forced first-upload failures must record `video_gpu_upload_retry`, remain on the
opaque background, and emit `presentation_resumed` only after the retained
current-generation frame uploads and draws successfully on retry.

The default timeout is 30 minutes. Override it with `-MaxMinutes 60` if the scan takes longer.
The expanded directory is retained beside the ZIP so its contents can be reviewed before
Expand Down
25 changes: 25 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,31 @@ macOS, and Linux share one QRhi lifecycle: the final native surface hierarchy is
prepared pre-show, while `ensure_feature("detail")` completes its non-native UI
and QtMultimedia runtime post-paint. Platform allowlists, post-show surface
creation, parentless surface warm-up, and hide/show workarounds are forbidden.
Windows OpenGL Python-module warm-up is enabled only after startup reaches its
terminal state and is triggered by a later Detail hover/click; it is never a
startup prerequisite. It uses a lazily created, isolated one-thread pool and
may retry one failed import on a later interaction without occupying still
preparation lanes. QRhi/context creation and GPU resource allocation remain on
the active Detail surface. Windows reveal waits for the additional QRhi
submission heuristic, but that wait is bounded and may fail open only after the
matching current-generation media content was already submitted.
Every still, native-video, and adjusted-video transition suppresses media draws
by generation before exposing its QRhi surface. Suppressed renders submit an
opaque clear frame while preserving still residency; only installation of
matching-generation content may resume media drawing. Media-specific overlays
remain suppressed until that generation reaches its reveal terminal.
Live Photo stills decoded while motion is active retain their request
generation and re-enter the same image transition/reveal terminal when motion
ends, so generation-bound overlays cannot remain deferred indefinitely.
After `VideoArea.begin_load()` suppresses both video renderers, the active
surface is explicitly updated under the Detail cover so its retained backing
texture is replaced by the opaque clear frame before decode completes. Video
suppression is released only after the new frame's GPU upload and draw are
recorded successfully. Upload failure keeps the retryable CPU frame and submits
only the opaque clear surface.
If asynchronous presentation preparation switches native/adjusted renderers,
the newly active renderer submits its own suppressed clear frame before any new
content is installed.
The pre-show hierarchy is part of shell construction: failure is terminal and
non-recoverable for that process. Startup-generation retry applies only after a
valid visible shell exists; it must not claim to reconstruct the native
Expand Down
19 changes: 19 additions & 0 deletions docs/requirements/DETAIL_OPEN_BENCHMARK_RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,26 @@ Phase 2 still 采样必须同时保留以下事件,并按 `asset_id + generati
- `decode_fallback`:统计 `imageio_to_qt`、`wic_to_qt`、`pillow`、`qt_full_scale`、`half`、`full`、`full_level`;没有 fallback
的样本也要计入分母。
- `surface_ready`:核对最终 detached surface 的宽高与 decode level。
- Windows/OpenGL 冷首开同时保留 `gl_runtime_preloaded`、
`image_surface_init_requested`、`qrhi_initialize_started/finished` 与
`presentation_suppressed/resumed`、`post_submit_scheduled/armed`。模块预热
只能由 startup terminal 之后的 hover/click 触发;不得出现在无用户交互的
startup profile 中。suppressed 区间不得产生旧 generation 的
`gpu_upload`、`presented` 或 media submission。
- video transition 必须在 active surface 确定后记录
`video_surface_blank_requested`;对应的 `presentation_resumed` 只能出现在
current-generation frame 的 GPU upload 和 draw 均成功之后。GPU upload
失败必须记录 `video_gpu_upload_retry`、保留 retryable frame 并只提交 opaque
clear。frame rotation/staging/install 失败时 suppression 必须保持有效。异步 preparation 切换 native/adjusted
renderer 时必须再记录 `reason=surface_switch`、当前 media generation 和最终
surface;正常非 transition 切换不得产生该事件。
- `presented`:仍是 click-to-present 的终点;stale generation 不得产生该事件。
- 正常 Windows reveal 记录 `surface_revealed(reason=composition)`;缺少额外
composition 时允许在匹配内容已提交后记录
`post_submit_deadline`、`surface_revealed(reason=deadline)`。过期 epoch、
generation、source 或 surface 只能产生 `post_submit_discarded`,不得 reveal。
快速切换主动取消 scheduled/armed barrier 时也必须记录 discard 的旧 epoch、
`state=scheduled|armed` 与取消 reason,不能只依赖 deadline 路径记录。
- RAW 冷解码同时保留 `raw_probe`、`raw_candidate_selected`、`raw_thumb_decode`、
`raw_postprocess`、`raw_surface_convert` 和 `color_stats`。未知几何必须先由 `raw_probe` 修复后再产生
`level_selected`;每个 cache miss 只能选择 embedded、half、full 之一,不得在同一请求中连续执行
Expand Down
5 changes: 5 additions & 0 deletions src/iPhoto/gui/coordinators/desktop_coordinator_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,11 @@ def start(self):
self._theme_controller.apply_current_theme()
self._view_router.show_gallery()

def enable_detail_interaction_warmup(self) -> None:
"""Enable demand-driven Detail imports after startup is complete."""

self._player_view_controller.enable_interaction_warmup()

# ------------------------------------------------------------------
# Lazy Edit lifecycle used by the Detail immersive port
# ------------------------------------------------------------------
Expand Down
11 changes: 9 additions & 2 deletions src/iPhoto/gui/coordinators/playback_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1272,7 +1272,9 @@ def _render_presentation(self, presentation: DetailPresentation) -> None:

if presentation.is_live:
self._hide_face_name_overlay(clear_annotations=False)
self._player_view.show_live_badge()
self._player_view.defer_live_badge_until_ready(
presentation.request_generation
)
self._player_view.set_live_replay_enabled(True)
self._autoplay_live_motion(presentation)
else:
Expand Down Expand Up @@ -1553,12 +1555,18 @@ def _restore_live_still(self, *, stop_motion: bool = False) -> bool:
still = self._active_live_still
asset_id = self._active_live_asset_id
transaction = getattr(self, "_detail_render_transaction", None)
badge_generation = (
transaction.generation
if transaction is not None
else getattr(self, "_detail_request_generation", 0)
)
if stop_motion:
self._player_view.video_area.stop()
self._active_live_motion = None
self._active_live_still = None
self._active_live_asset_id = ""
self._active_live_media_generation = None
self._player_view.defer_live_badge_until_ready(badge_generation)
self._player_view.defer_still_updates(False)
if not self._player_view.apply_pending_still():
if (
Expand All @@ -1573,7 +1581,6 @@ def _restore_live_still(self, *, stop_motion: bool = False) -> bool:
else:
self._player_view.display_image(still, asset_id=asset_id)
self._player_bar.setEnabled(False)
self._player_view.show_live_badge()
self._player_view.set_live_replay_enabled(True)
self._is_playing = False
return True
Expand Down
7 changes: 7 additions & 0 deletions src/iPhoto/gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,13 @@ def _run_idle_startup_jobs() -> None:
_logger.info("Starting deferred startup scan")
starter()
startup.complete()
enable_detail_warmup = getattr(
coordinator_runtime,
"enable_detail_interaction_warmup",
None,
)
if callable(enable_detail_warmup):
enable_detail_warmup()

def _enqueue_idle_startup_jobs() -> None:
nonlocal startup_scan_enqueued
Expand Down
Loading
Loading