diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 70e20000..296f8f6b 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -46,6 +46,20 @@ TDD + review loop). Estimate for tiers 1-4 ≈ 3-4 working days; tier 5 ≈ 5-8 ## Tier 0 — owner actions (start the clocks) +**New observations, awaiting owner ranking (2026-09-14):** + +- [#526](https://github.com/iamfatness/CoreVideoPro/issues/526): intermittent Program-buffer + delivery failures. PR #528 moves startup resource allocation before the render clock + and adds failure-stage diagnostics. The earlier steady-state failures remain unresolved. +- [#529](https://github.com/iamfatness/CoreVideoPro/issues/529): ISO recording queue loses + video during simultaneous 1080p60 streaming, including a stationary eight-person wall. + Files finalize, but completion/healthy status does not establish loss-free recording. +- [#530](https://github.com/iamfatness/CoreVideoPro/issues/530): the control API advertises + `programPreview`, but the implemented view name is `program-preview`; the advertised + name silently selects Program. Live QA uses and verifies the implemented name. + +These entries record findings; they do not change the owner-approved order below. + | ID | Item | Clause | Why it is first | |---|---|---|---| | [T0.1](https://github.com/iamfatness/CoreVideoPro/issues/423) | Buy the code-signing certificate (Authenticode / Trusted Signing) | 4 | `release.yml` already refuses to ship unsigned; the 09-08 beta shipped unsigned (SmartScreen). Org validation takes weeks — the calendar critical path. | diff --git a/native/src/core/MediaCore.cpp b/native/src/core/MediaCore.cpp index cccc63b0..2a140d48 100644 --- a/native/src/core/MediaCore.cpp +++ b/native/src/core/MediaCore.cpp @@ -6940,6 +6940,7 @@ void MediaCore::enableAudioOutputWorker() { const auto* configured = std::getenv("COREVIDEO_PROGRAM_BUFFER_FRAMES"); const int frames = configured && std::string_view(configured) == "2" ? 2 : 3; modules_.compositor->configureProgramBuffer(frames); + modules_.compositor->prepareProgramBuffer(outputWidth_, outputHeight_); publishProgramOutputConfiguration(); audioWorkerActive_ = true; } diff --git a/native/src/modules/D3D11CompositorAdapter.cpp b/native/src/modules/D3D11CompositorAdapter.cpp index a7145fc1..de874ed8 100644 --- a/native/src/modules/D3D11CompositorAdapter.cpp +++ b/native/src/modules/D3D11CompositorAdapter.cpp @@ -121,6 +121,12 @@ class D3D11Compositor final : public ICompositor { std::string rendererName() const override { return "d3d11"; } void configureProgramBuffer(int frames) override { requestedProgramFrames_.store(frames == 2 ? 2 : 3); } + void prepareProgramBuffer(int width, int height) override { + // Called before the display worker establishes its cadence anchor. Device, + // texture and shader creation must not consume slot zero's delivery lead. + if (width > 0 && height > 0 && ensureRenderTarget(width, height)) + (void)ensureProgramBuffer(width, height); + } int programBufferFrames() const override { return requestedProgramFrames_.load(); } void setProgramProductionTiming(int64_t slot, int64_t anchorNs) override { programProductionSlot_ = slot; programProductionAnchorNs_ = anchorNs; @@ -237,17 +243,7 @@ class D3D11Compositor final : public ICompositor { } const auto evictUs = stageUs(); if (buffered) { - auto buffer = currentProgramBuffer(); - if (!buffer || !buffer->dimensions(frame.width, frame.height, programBufferFrames())) { - buffer = std::make_shared(device_.get(), frame.width, frame.height, programBufferFrames(), ++programBufferGeneration_, - [this](const ProgramFrame& delivered) { - if (!delivered.programNv12Shared) return; - std::lock_guard lock(vcamSinkMutex_); - if (vcamSink_) vcamSink_(delivered.programNv12Shared, delivered.programNv12Width, delivered.programNv12Height); - }); - std::shared_ptr retired; - { std::lock_guard lock(programBufferMutex_); retired = std::exchange(programBuffer_, buffer); } - } + auto buffer = ensureProgramBuffer(frame.width, frame.height); frame.producedAt100ns = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count() / 100; frame.productionSlot = programProductionSlot_; frame.productionAnchorNs = programProductionAnchorNs_; frame.renderPlanEvidence = std::make_shared(std::move(deterministicPlan)); @@ -436,6 +432,19 @@ class D3D11Compositor final : public ICompositor { std::shared_ptr currentProgramBuffer() const { std::lock_guard lock(programBufferMutex_); return programBuffer_; } + std::shared_ptr ensureProgramBuffer(int width, int height) { + auto buffer = currentProgramBuffer(); + if (buffer && buffer->dimensions(width, height, programBufferFrames())) return buffer; + buffer = std::make_shared(device_.get(), width, height, programBufferFrames(), ++programBufferGeneration_, + [this](const ProgramFrame& delivered) { + if (!delivered.programNv12Shared) return; + std::lock_guard lock(vcamSinkMutex_); + if (vcamSink_) vcamSink_(delivered.programNv12Shared, delivered.programNv12Width, delivered.programNv12Height); + }); + std::shared_ptr retired; + { std::lock_guard lock(programBufferMutex_); retired = std::exchange(programBuffer_, buffer); } + return buffer; + } ID3D11ShaderResourceView* retainedProgramForMultiview() { auto buffer = currentProgramBuffer(); ProgramFrameSharedTexture exported; diff --git a/native/src/modules/D3DProgramBuffer.h b/native/src/modules/D3DProgramBuffer.h index 30c69b13..c31e4908 100644 --- a/native/src/modules/D3DProgramBuffer.h +++ b/native/src/modules/D3DProgramBuffer.h @@ -313,7 +313,13 @@ class D3DProgramBuffer { }); if (stopped_) break; if (delivery_.empty() || delivery_.front()->state != State::Ready || delivery_.front()->productionSlot != targetSlot) { - if (const auto due = timeline_->takeDue(now100ns() * 100)) diagnostics_.underruns += due->skippedSlots + 1; + if (const auto due = timeline_->takeDue(now100ns() * 100)) { + diagnostics_.underruns += due->skippedSlots + 1; + ::corevideo::core::nativeLogf("[program-buffer-miss] stage=source target=%lld front=%lld state=%d skipped=%lld late_ns=%lld\n", + static_cast(targetSlot), delivery_.empty() ? -1LL : static_cast(delivery_.front()->productionSlot), + delivery_.empty() ? -1 : static_cast(delivery_.front()->state), static_cast(due->skippedSlots), + static_cast(now100ns() * 100 - due->deadlineNs)); + } continue; } // Prepare a private GPU image early. Stable monitor exports remain readable @@ -348,7 +354,12 @@ class D3DProgramBuffer { maximumCopyNs_ = (std::max)(maximumCopyNs_, static_cast(copyNs)); minimumPreparationLeadNs_ = (std::min)(minimumPreparationLeadNs_, static_cast(preparationLeadNs)); maximumCompletionLateNs_ = (std::max)(maximumCompletionLateNs_, static_cast(completionLateNs)); - if (completed > deadline) ++diagnostics_.deadlineMisses; + if (completed > deadline) { + ++diagnostics_.deadlineMisses; + ::corevideo::core::nativeLogf("[program-buffer-miss] stage=gpu-copy slot=%lld lead_ns=%lld copy_ns=%lld late_ns=%lld\n", + static_cast(slot->productionSlot), static_cast(preparationLeadNs), + static_cast(copyNs), static_cast(completionLateNs)); + } if (!stopped_) changed_.wait_until(lock, deadline, [&] { return stopped_; }); const auto due = timeline_->takeDue(now100ns() * 100); const bool current = due && due->slot == slot->productionSlot; @@ -395,6 +406,10 @@ class D3DProgramBuffer { diagnostics_.underruns += missed->skippedSlots + 1; const bool deliveryExpired = exportExpired || now100ns() * 100 >= expiresAtNs; if (!current || deliveryExpired) { + ::corevideo::core::nativeLogf("[program-buffer-miss] stage=publish slot=%lld current=%d expired=%d export_ns=%lld late_ns=%lld\n", + static_cast(slot->productionSlot), current, deliveryExpired, + static_cast(std::chrono::duration_cast(exportEnd - exportBegin).count()), + static_cast(now100ns() * 100 - timeline_->deadlineNs(slot->productionSlot))); if (current) ++diagnostics_.underruns; else if (due) ++diagnostics_.underruns; // The selected due slot had no delivered packet. if (shellCopied || multiviewCopied) latest_.reset(); // Export contents no longer prove the old snapshot. diff --git a/native/src/modules/Interfaces.h b/native/src/modules/Interfaces.h index 7254058b..5578d723 100644 --- a/native/src/modules/Interfaces.h +++ b/native/src/modules/Interfaces.h @@ -808,6 +808,9 @@ class ICompositor { // Startup-only configuration. Unsupported compositors report zero active // frames, so consumers must not introduce an unmatched audio delay. virtual void configureProgramBuffer(int /*frames*/) {} + // Allocate startup resources before the render clock starts. No frames or + // delivery timestamps may be produced by this call. Resize stays on render. + virtual void prepareProgramBuffer(int /*width*/, int /*height*/) {} virtual void setProgramProductionTiming(int64_t /*slot*/, int64_t /*anchorNs*/) {} [[nodiscard]] virtual int programBufferFrames() const { return 0; } virtual bool latestDeliveredProgramFrame(ProgramFrame& /*out*/) const { return false; } diff --git a/native/tests/D3DProgramBufferTest.cpp b/native/tests/D3DProgramBufferTest.cpp index 0dad7cba..5078cf96 100644 --- a/native/tests/D3DProgramBufferTest.cpp +++ b/native/tests/D3DProgramBufferTest.cpp @@ -5,6 +5,31 @@ #include #if defined(_WIN32) && !COREVIDEO_STUB && COREVIDEO_ENABLE_DEV_ADAPTERS && COREVIDEO_WITH_D3D11 +TEST(D3DProgramBuffer, StartupPreparationAllocatesWithoutStartingDeliveryClock) { + auto compositor = corevideo::modules::createD3D11Compositor(); + ASSERT_TRUE(compositor != nullptr); + compositor->configureProgramBuffer(3); + compositor->prepareProgramBuffer(1920, 1080); + const auto prepared = compositor->programBufferDiagnostics(); + EXPECT_TRUE(prepared.generation > 0); + EXPECT_EQ(prepared.status, "priming"); + EXPECT_EQ(prepared.produced, 0u); + EXPECT_EQ(prepared.delivered, 0u); + // Startup can wait indefinitely for the worker: allocation cannot start a + // hidden cadence or charge idle startup time as missed Program frames. + std::this_thread::sleep_for(std::chrono::milliseconds(80)); + EXPECT_EQ(compositor->programBufferDiagnostics().underruns, 0u); + corevideo::modules::ProgramFrame frame; + EXPECT_FALSE(compositor->takeDeliveredProgramFrame(frame, 0)); + corevideo::modules::CompositorRenderPlan plan; + plan.width = 1920; plan.height = 1080; plan.skipCpuReadback = true; + (void)compositor->render(plan, {}); + EXPECT_EQ(compositor->programBufferDiagnostics().generation, prepared.generation); + ASSERT_TRUE(compositor->takeDeliveredProgramFrame(frame, 1500)); + EXPECT_EQ(frame.width, 1920); + EXPECT_EQ(frame.height, 1080); +} + TEST(D3DProgramBuffer, RetainsTaggedNv12AndDeliversWithoutFurtherRendering) { for (const int depth : {2, 3}) { auto compositor = corevideo::modules::createD3D11Compositor();