Skip to content
Open
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
17 changes: 11 additions & 6 deletions docs/dolby_vision_profile81.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,13 @@ public:
};

/// L1 元数据推导:avg 取 stats.avg_maxrgb_pq(PQ 域均值),
/// max 取 percentile_99(离群点防护),min 取 percentile_10_pq 后钳位。
/// max 取 percentile_99(离群点防护)。有扩展近黑统计时,近黑覆盖率达到 1%
/// 才把 min 报告为零,否则取 percentile_1_pq;旧分析结果回退到 percentile_10_pq。
/// 所有值随后钳位。
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 = false);

/// frame_id 绑定的固定容量在途 RPU 队列:编码输出按 frame_index 取回对应 RPU,
/// 超过最大在途帧数返回失败 —— 调用方应停止 DV 而不是错位附接。
Expand Down Expand Up @@ -180,8 +184,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 +429,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
46 changes: 38 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,27 @@ 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();
pending_scene_refresh_ = true;
}
luminance_filter_.update(stats);
if (const auto metadata = frame_metadata_from_stats(
luminance_filter_.smoothed(stats), pending_scene_refresh_)) {
last_metadata_ = metadata;
// Consume the refresh only after a valid RPU can carry it. An invalid
// analyzer sample must not strand the new scene without a refresh.
pending_scene_refresh_ = false;
}
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,7 +523,10 @@ namespace video::dolby_vision {
void
rpu_injector_t::disable() {
enabled_ = false;
scene_detector_.reset();
luminance_filter_.reset();
last_metadata_.reset();
pending_scene_refresh_ = false;
queue_.clear();
generator_.reset();
}
Expand Down
Loading