Skip to content
Draft
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
14 changes: 14 additions & 0 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
1 change: 1 addition & 0 deletions native/src/core/MediaCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
31 changes: 20 additions & 11 deletions native/src/modules/D3D11CompositorAdapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<D3DProgramBuffer>(device_.get(), frame.width, frame.height, programBufferFrames(), ++programBufferGeneration_,
[this](const ProgramFrame& delivered) {
if (!delivered.programNv12Shared) return;
std::lock_guard<std::mutex> lock(vcamSinkMutex_);
if (vcamSink_) vcamSink_(delivered.programNv12Shared, delivered.programNv12Width, delivered.programNv12Height);
});
std::shared_ptr<D3DProgramBuffer> retired;
{ std::lock_guard<std::mutex> lock(programBufferMutex_); retired = std::exchange(programBuffer_, buffer); }
}
auto buffer = ensureProgramBuffer(frame.width, frame.height);
frame.producedAt100ns = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count() / 100;
frame.productionSlot = programProductionSlot_; frame.productionAnchorNs = programProductionAnchorNs_;
frame.renderPlanEvidence = std::make_shared<const CompositorRenderPlan>(std::move(deterministicPlan));
Expand Down Expand Up @@ -436,6 +432,19 @@ class D3D11Compositor final : public ICompositor {
std::shared_ptr<D3DProgramBuffer> currentProgramBuffer() const {
std::lock_guard<std::mutex> lock(programBufferMutex_); return programBuffer_;
}
std::shared_ptr<D3DProgramBuffer> ensureProgramBuffer(int width, int height) {
auto buffer = currentProgramBuffer();
if (buffer && buffer->dimensions(width, height, programBufferFrames())) return buffer;
buffer = std::make_shared<D3DProgramBuffer>(device_.get(), width, height, programBufferFrames(), ++programBufferGeneration_,
[this](const ProgramFrame& delivered) {
if (!delivered.programNv12Shared) return;
std::lock_guard<std::mutex> lock(vcamSinkMutex_);
if (vcamSink_) vcamSink_(delivered.programNv12Shared, delivered.programNv12Width, delivered.programNv12Height);
});
std::shared_ptr<D3DProgramBuffer> retired;
{ std::lock_guard<std::mutex> lock(programBufferMutex_); retired = std::exchange(programBuffer_, buffer); }
return buffer;
}
ID3D11ShaderResourceView* retainedProgramForMultiview() {
auto buffer = currentProgramBuffer();
ProgramFrameSharedTexture exported;
Expand Down
19 changes: 17 additions & 2 deletions native/src/modules/D3DProgramBuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<long long>(targetSlot), delivery_.empty() ? -1LL : static_cast<long long>(delivery_.front()->productionSlot),
delivery_.empty() ? -1 : static_cast<int>(delivery_.front()->state), static_cast<long long>(due->skippedSlots),
static_cast<long long>(now100ns() * 100 - due->deadlineNs));
}
continue;
}
// Prepare a private GPU image early. Stable monitor exports remain readable
Expand Down Expand Up @@ -348,7 +354,12 @@ class D3DProgramBuffer {
maximumCopyNs_ = (std::max)(maximumCopyNs_, static_cast<int64_t>(copyNs));
minimumPreparationLeadNs_ = (std::min)(minimumPreparationLeadNs_, static_cast<int64_t>(preparationLeadNs));
maximumCompletionLateNs_ = (std::max)(maximumCompletionLateNs_, static_cast<int64_t>(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<long long>(slot->productionSlot), static_cast<long long>(preparationLeadNs),
static_cast<long long>(copyNs), static_cast<long long>(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;
Expand Down Expand Up @@ -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<long long>(slot->productionSlot), current, deliveryExpired,
static_cast<long long>(std::chrono::duration_cast<std::chrono::nanoseconds>(exportEnd - exportBegin).count()),
static_cast<long long>(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.
Expand Down
3 changes: 3 additions & 0 deletions native/src/modules/Interfaces.h
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
25 changes: 25 additions & 0 deletions native/tests/D3DProgramBufferTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,31 @@
#include <cmath>

#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();
Expand Down
Loading