Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 5 additions & 4 deletions docs/dolby_vision_profile81.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ L1 偏移也相同 —— 但这只是巧合,不应成为依赖。`configure()
现有统计不能机械复制成 DV 的语义:

- **`min_pq` 不能用绝对最小像素。** 游戏中一个黑色 UI 像素、黑边或透明合成区域就能
把整帧最小值钉死在零。用「原始最小值 + 低百分位亮度 + 黑像素比例 + 时间稳定器」
共同生成稳健最小值
把整帧最小值钉死在零。分析器同时给出 PQ 第 1 百分位和首个 PQ 直方图 bin 的覆盖率;
覆盖率达到 1% 才报告零,否则采用第 1 百分位。旧分析结果没有扩展统计时回退到 P10
- **`avg_pq` 必须是 PQ 域平均,不是 `average_maxrgb`。** PQ 是凹函数,
`PQ(mean(nits)) ≥ mean(PQ(nits))`,暗场带高光时差距是整个动态范围的大部分。
现有 `stats.avg_maxrgb_pq` 正是逐像素累加的 PQ 域平均,直接用它。
Expand Down Expand Up @@ -425,8 +425,9 @@ OPPO 真机解码链路 + Sony 电视 Dolby Vision 点亮);主机侧灰度
统计缺失时的保守 RPU、RPU/frame_id 严格匹配、零分配优化。

已落地:avg/max/min 推导与钳位(§3.2)、统计缺失复用上次有效值、首帧预热跳过、
frame_index 严格绑定、队列溢出即停、稳态零分配。待实机调优:场景切换信号
(当前 scene_refresh 恒 false,EMA 的切场检测尚未导出)。
frame_index 严格绑定、队列溢出即停、稳态零分配。`scene_refresh` 由独立 GPU 样本的
PQ 均值、P10/P90 与 HDR10+ 分位分布共同判定;重复使用同一分析样本不会重复刷新。
待实机调优:按游戏类型校准切场阈值与近黑覆盖率阈值。

### Phase 3:正式协议协商与降级(协商层已落地)

Expand Down
8 changes: 8 additions & 0 deletions src/platform/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,12 @@ namespace platf {
/// and the histogram is point-sampled per analysis cell. Zero alongside a nonzero
/// avg_maxrgb means the analyzer did not produce one, which HDR Vivid rejects.
float avg_maxrgb_pq = 0.0f;
/// First percentile and near-black coverage from the analyzer's PQ histogram.
/// `near_black_fraction` is the fraction in histogram bin zero (PQ < 1/256).
/// The validity bit distinguishes an older analyzer from a genuinely black frame.
float percentile_1_pq = 0.0f;
float near_black_fraction = 0.0f;
bool near_black_stats_valid = false;
float percentile_10_pq = 0.0f; ///< 10th percentile in normalized PQ signal space
float percentile_90_pq = 0.0f; ///< 90th percentile in normalized PQ signal space
/// 99th percentile of maxRGB (nits). Reported as the HDR10+ maxSCL; see
Expand All @@ -531,6 +537,8 @@ namespace platf {
float distribution_maxrgb[HDR10PLUS_PERCENTILES] = {};
float analysis_max_nits = 0.0f; ///< Upper luminance bound used by the analyzer
uint64_t sample_sequence = 0; ///< Increments only when a new GPU readback completes
uint64_t analyzed_frame_sequence = 0; ///< Capture-side frame sequence represented by this sample
uint32_t sample_age_frames = 0; ///< Capture frames elapsed since analyzed_frame_sequence
bool valid = false; ///< Whether stats are available (false on first frame)
};

Expand Down
115 changes: 88 additions & 27 deletions src/platform/windows/display_vram.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -417,19 +417,25 @@ namespace platf::dxgi {
if (!img.blank) {
auto &img_ctx = img_ctx_map[img.id];
const bool can_analyze_hdr_frame = hdr_analysis_enabled && img.linear_gamma && img.format == DXGI_FORMAT_R16G16B16A16_FLOAT;
const uint64_t hdr_current_frame_sequence = ++hdr_capture_frame_sequence;

// Open the shared capture texture with our ID3D11Device
if (initialize_image_context(img, img_ctx)) {
return -1;
}

// Poll the previous analysis result before taking the capture mutex.
if (hdr_analysis_pending) {
read_hdr_analysis_results();
if (hdr_luminance_stats_out.valid && !runtime_status.scene_metadata_active) {
runtime_status.scene_metadata_active = true;
::video::update_hdr_pipeline_status(runtime_status_id, runtime_status);
}
read_hdr_analysis_results(hdr_current_frame_sequence);
if (hdr_luminance_stats_out.valid) {
const uint64_t age = hdr_current_frame_sequence >= hdr_luminance_stats_out.analyzed_frame_sequence ?
hdr_current_frame_sequence - hdr_luminance_stats_out.analyzed_frame_sequence :
0;
hdr_luminance_stats_out.sample_age_frames = static_cast<uint32_t>(
std::min<uint64_t>(age, std::numeric_limits<uint32_t>::max()));
}
if (hdr_luminance_stats_out.valid && !runtime_status.scene_metadata_active) {
runtime_status.scene_metadata_active = true;
::video::update_hdr_pipeline_status(runtime_status_id, runtime_status);
}

// Acquire encoder mutex to synchronize with capture code. Normal
Expand Down Expand Up @@ -600,7 +606,7 @@ namespace platf::dxgi {
}

if (hdr_analysis_source) {
dispatch_hdr_analysis(hdr_analysis_source);
dispatch_hdr_analysis(hdr_analysis_source, hdr_current_frame_sequence);
}
}

Expand Down Expand Up @@ -788,8 +794,10 @@ namespace platf::dxgi {
::video::unregister_hdr_pipeline_status(runtime_status_id);
runtime_status_id = 0;
hdr_luminance_stats_out = {};
hdr_analysis_pending = false;
hdr_staging_pending.fill(false);
hdr_staging_frame_sequences.fill(0);
hdr_analysis_frame_index = 0;
hdr_capture_frame_sequence = 0;
hdr_analysis_sample_sequence = 0;

// init() builds the analyzer from the pixel format alone, because the
Expand Down Expand Up @@ -1712,16 +1720,20 @@ namespace platf::dxgi {
uav_t hdr_final_result_uav; // UAV view for pass 2 output
buf_t hdr_global_histogram_buf; // 256-bin PQ histogram accumulated by pass 1 atomics
uav_t hdr_global_histogram_uav; // Typed R32_UINT UAV (clearable + atomic-capable)
buf_t hdr_staging_buf; // Staging buffer for CPU readback (1 FinalResult)
// A small ring prevents a slow asynchronous Map() from making a later
// CopyResource overwrite the still-pending result it was meant to read.
std::array<buf_t, 4> hdr_staging_bufs;
std::array<uint64_t, 4> hdr_staging_frame_sequences {};
std::array<bool, 4> hdr_staging_pending {};
buf_t hdr_analysis_cbuf; // Constant buffer for pass 1 (analysis resolution)
buf_t hdr_analysis_snapshot_cbuf; // Shared converter/pass 1 params for the snapshot
buf_t hdr_reduce_cbuf; // Constant buffer for pass 2 (numGroups)
uint32_t hdr_analysis_width = 0; // Analysis grid width (downsampled from source)
uint32_t hdr_analysis_height = 0; // Analysis grid height (downsampled from source)
uint32_t hdr_num_groups = 0; // Number of thread groups dispatched in pass 1
uint64_t hdr_analysis_frame_index = 0; // Used to downsample analysis frequency
uint64_t hdr_capture_frame_sequence = 0; // Counts captured frames presented to conversion
uint64_t hdr_analysis_sample_sequence = 0; // Counts completed, independent GPU samples
bool hdr_analysis_pending = false; // Whether we have results ready to read
bool hdr_analysis_ready = false; // Whether the analyzer's GPU resources were created
bool hdr_analysis_enabled = false; // Whether analysis runs: resources exist and the stream can carry metadata
::video::hdr_metadata::formats_t hdr_metadata_formats; // Dynamic metadata formats this stream may carry
Expand Down Expand Up @@ -1996,16 +2008,18 @@ namespace platf::dxgi {
return -1;
}

// --- Staging buffer for async CPU readback (1 FinalResult only) ---
// --- Staging ring for asynchronous CPU readback ---
D3D11_BUFFER_DESC staging_desc = {};
staging_desc.ByteWidth = sizeof(FinalResult);
staging_desc.Usage = D3D11_USAGE_STAGING;
staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;

status = device->CreateBuffer(&staging_desc, nullptr, &hdr_staging_buf);
if (FAILED(status)) {
BOOST_LOG(warning) << "Failed to create HDR staging buffer: " << util::log_hex(status);
return -1;
for (auto &staging_buffer : hdr_staging_bufs) {
status = device->CreateBuffer(&staging_desc, nullptr, &staging_buffer);
if (FAILED(status)) {
BOOST_LOG(warning) << "Failed to create HDR staging buffer: " << util::log_hex(status);
return -1;
}
}

// Resources exist; whether they get used is init_output()'s call, once the
Expand All @@ -2015,7 +2029,8 @@ namespace platf::dxgi {
<< ", analysis " << hdr_analysis_width << "x" << hdr_analysis_height
<< ", " << hdr_num_groups << " groups (" << groups_x << "x" << groups_y << ")"
<< ", interval 1/" << HDR_ANALYSIS_INTERVAL
<< ", staging: " << sizeof(FinalResult) << " bytes";
<< ", staging ring: " << hdr_staging_bufs.size() << " x "
<< sizeof(FinalResult) << " bytes";
return 0;
}

Expand Down Expand Up @@ -2052,9 +2067,16 @@ namespace platf::dxgi {
* @param source Unified full-frame or snapshot analysis input.
*/
void
dispatch_hdr_analysis(const HdrAnalysisSource &source) {
dispatch_hdr_analysis(const HdrAnalysisSource &source, uint64_t frame_sequence) {
if (!hdr_analysis_enabled || !source) return;

const auto free_slot = std::find(hdr_staging_pending.begin(), hdr_staging_pending.end(), false);
if (free_slot == hdr_staging_pending.end()) {
BOOST_LOG(debug) << "HDR analysis readback ring full; skipping capture frame " << frame_sequence;
return;
}
const size_t staging_index = static_cast<size_t>(free_slot - hdr_staging_pending.begin());

// Unbind render targets to avoid resource hazard (SRV vs RTV conflict)
ID3D11RenderTargetView *null_rtv = nullptr;
device_ctx->OMSetRenderTargets(1, &null_rtv, nullptr);
Expand Down Expand Up @@ -2108,10 +2130,11 @@ namespace platf::dxgi {
device_ctx->CSSetConstantBuffers(0, 1, &null_cb);
device_ctx->CSSetShader(nullptr, nullptr, 0);

// Copy final result to staging buffer for CPU readback next frame
device_ctx->CopyResource(hdr_staging_buf.get(), hdr_final_result_buf.get());

hdr_analysis_pending = true;
// Capture this dispatch in its own staging slot. GPU command ordering keeps
// the shared final buffer safe while the CPU may still be reading older slots.
device_ctx->CopyResource(hdr_staging_bufs[staging_index].get(), hdr_final_result_buf.get());
hdr_staging_frame_sequences[staging_index] = frame_sequence;
hdr_staging_pending[staging_index] = true;
}

/**
Expand All @@ -2120,23 +2143,48 @@ namespace platf::dxgi {
* and computes PQ-domain percentiles from the histogram.
*/
void
read_hdr_analysis_results() {
read_hdr_analysis_results(uint64_t current_frame_sequence) {
while (true) {
size_t staging_index = hdr_staging_pending.size();
for (size_t i = 0; i < hdr_staging_pending.size(); ++i) {
if (hdr_staging_pending[i] &&
(staging_index == hdr_staging_pending.size() ||
hdr_staging_frame_sequences[i] < hdr_staging_frame_sequences[staging_index])) {
staging_index = i;
}
}
if (staging_index == hdr_staging_pending.size()) {
return;
}

if (!read_hdr_analysis_result(staging_index, current_frame_sequence)) {
return; // Oldest result is still on the GPU; later copies cannot be ready yet.
}
}
}

bool
read_hdr_analysis_result(size_t staging_index, uint64_t current_frame_sequence) {
D3D11_MAPPED_SUBRESOURCE mapped = {};
HRESULT status = device_ctx->Map(hdr_staging_buf.get(), 0, D3D11_MAP_READ, D3D11_MAP_FLAG_DO_NOT_WAIT, &mapped);
HRESULT status = device_ctx->Map(
hdr_staging_bufs[staging_index].get(), 0,
D3D11_MAP_READ, D3D11_MAP_FLAG_DO_NOT_WAIT, &mapped);

if (status == DXGI_ERROR_WAS_STILL_DRAWING) {
// GPU hasn't finished yet — skip this readback, try next frame
return;
return false;
}

if (FAILED(status)) {
BOOST_LOG(debug) << "HDR staging Map failed: " << util::log_hex(status);
return;
hdr_staging_pending[staging_index] = false;
return true;
}

auto *result = reinterpret_cast<const FinalResult *>(mapped.pData);

if (result->pixelCount > 0) {
hdr_luminance_stats_out = {};
hdr_luminance_stats_out.min_maxrgb = result->minMaxRGB;
hdr_luminance_stats_out.max_maxrgb = result->maxMaxRGB;
hdr_luminance_stats_out.avg_maxrgb = result->sumMaxRGB / static_cast<float>(result->pixelCount);
Expand Down Expand Up @@ -2164,6 +2212,9 @@ namespace platf::dxgi {
// Retain P99 in nits for the independent HDR10+ path, and fill the nine
// percentiles ST 2094-40 deployment profiles carry from the same walk.
const uint32_t total = result->pixelCount;
hdr_luminance_stats_out.near_black_fraction =
static_cast<float>(result->histogram[0]) / static_cast<float>(total);
hdr_luminance_stats_out.near_black_stats_valid = true;
const auto &percentages = ::video::hdr_metadata::hdr10plus_percentages;
constexpr size_t kDistCount = percentages.size();

Expand All @@ -2187,6 +2238,9 @@ namespace platf::dxgi {
if (!dist_found[p] && cumulative >= dist_targets[p]) {
hdr_luminance_stats_out.distribution_maxrgb[p] =
::video::hdr_metadata::pq_to_nits(pq_bin_center);
if (p == 0) {
hdr_luminance_stats_out.percentile_1_pq = pq_bin_center;
}
dist_found[p] = true;
}
}
Expand All @@ -2212,11 +2266,18 @@ namespace platf::dxgi {

hdr_luminance_stats_out.analysis_max_nits = hdr_analysis_max_nits;
hdr_luminance_stats_out.sample_sequence = ++hdr_analysis_sample_sequence;
hdr_luminance_stats_out.analyzed_frame_sequence = hdr_staging_frame_sequences[staging_index];
const uint64_t age = current_frame_sequence >= hdr_luminance_stats_out.analyzed_frame_sequence ?
current_frame_sequence - hdr_luminance_stats_out.analyzed_frame_sequence :
0;
hdr_luminance_stats_out.sample_age_frames = static_cast<uint32_t>(
std::min<uint64_t>(age, std::numeric_limits<uint32_t>::max()));
hdr_luminance_stats_out.valid = true;
}

device_ctx->Unmap(hdr_staging_buf.get(), 0);
hdr_analysis_pending = false;
device_ctx->Unmap(hdr_staging_bufs[staging_index].get(), 0);
hdr_staging_pending[staging_index] = false;
return true;
}

// ===== Compute-shader RGB->P010 fast path (Phase 1) =====
Expand Down
41 changes: 33 additions & 8 deletions src/video_dolby_vision.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -152,19 +152,27 @@ namespace video::dolby_vision {
}

std::optional<frame_metadata_t>
frame_metadata_from_stats(const platf::hdr_frame_luminance_stats_t &stats) {
frame_metadata_from_stats(
const platf::hdr_frame_luminance_stats_t &stats,
bool scene_refresh) {
if (!stats.valid) {
return std::nullopt;
}

const bool finite = std::isfinite(stats.avg_maxrgb_pq) &&
std::isfinite(stats.avg_maxrgb) &&
std::isfinite(stats.percentile_99) &&
std::isfinite(stats.percentile_10_pq);
std::isfinite(stats.percentile_10_pq) &&
(!stats.near_black_stats_valid ||
(std::isfinite(stats.percentile_1_pq) &&
std::isfinite(stats.near_black_fraction)));
if (!finite ||
stats.avg_maxrgb_pq < 0.0f || stats.avg_maxrgb_pq > 1.0f ||
stats.percentile_99 < 0.0f ||
stats.percentile_10_pq < 0.0f || stats.percentile_10_pq > 1.0f) {
stats.percentile_10_pq < 0.0f || stats.percentile_10_pq > 1.0f ||
(stats.near_black_stats_valid &&
(stats.percentile_1_pq < 0.0f || stats.percentile_1_pq > 1.0f ||
stats.near_black_fraction < 0.0f || stats.near_black_fraction > 1.0f))) {
return std::nullopt;
}
// Zero PQ-domain mean beside a positive linear mean means the analyzer
Expand All @@ -174,11 +182,17 @@ namespace video::dolby_vision {
}

frame_metadata_t raw;
raw.min_pq = pq_signal_u12_rounded(stats.percentile_10_pq);
constexpr float meaningful_near_black_coverage = 0.01f;
const float robust_min_pq = stats.near_black_stats_valid ?
(stats.near_black_fraction >= meaningful_near_black_coverage ?
0.0f : stats.percentile_1_pq) :
stats.percentile_10_pq;
raw.min_pq = pq_signal_u12_rounded(robust_min_pq);
raw.max_pq = pq_code_u12_rounded(stats.percentile_99);
raw.avg_pq = pq_signal_u12_rounded(stats.avg_maxrgb_pq);
raw.scene_refresh = false;
return clamp_level1(raw.min_pq, raw.max_pq, raw.avg_pq);
auto result = clamp_level1(raw.min_pq, raw.max_pq, raw.avg_pq);
result.scene_refresh = scene_refresh;
return result;
}

bool
Expand Down Expand Up @@ -459,14 +473,23 @@ namespace video::dolby_vision {
// Missing analysis reuses the last good values: once RPUs are flowing,
// a frame without one would make the client's Dolby engine fall back to
// static HDR10 mapping for that frame — a visible brightness step.
if (const auto metadata = frame_metadata_from_stats(stats)) {
const auto scene = scene_detector_.observe(stats);
if (scene.new_sample && scene.scene_change) {
luminance_filter_.reset();
}
luminance_filter_.update(stats);
if (const auto metadata = frame_metadata_from_stats(
luminance_filter_.smoothed(stats), scene.scene_change)) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
last_metadata_ = metadata;
}
if (!last_metadata_) {
return; // cold analyzer: this frame ships without an RPU, like HDR10+ does
}

if (!queue_.stage(frame_index, generator_, *last_metadata_)) {
const auto metadata_for_frame = *last_metadata_;
// Reused luminance is intentional, but a refresh belongs to one picture.
last_metadata_->scene_refresh = false;
if (!queue_.stage(frame_index, generator_, metadata_for_frame)) {
// In-flight overflow means the encoder's output can no longer be
// trusted to surface in order; stop rather than risk a stale RPU
// landing on a newer picture (docs §3.5).
Expand Down Expand Up @@ -496,6 +519,8 @@ namespace video::dolby_vision {
void
rpu_injector_t::disable() {
enabled_ = false;
scene_detector_.reset();
luminance_filter_.reset();
last_metadata_.reset();
queue_.clear();
generator_.reset();
Expand Down
Loading