Skip to content

hdr: correct HDR10 (PQ/BT.2020) -> SDR tone-map primitive + test#21

Merged
fxd0h merged 2 commits into
mainfrom
feat/hdr-tonemap
Jun 26, 2026
Merged

hdr: correct HDR10 (PQ/BT.2020) -> SDR tone-map primitive + test#21
fxd0h merged 2 commits into
mainfrom
feat/hdr-tonemap

Conversation

@fxd0h

@fxd0h fxd0h commented Jun 26, 2026

Copy link
Copy Markdown
Owner

First piece of #16 (does not close it). Adds the correct HDR10->SDR conversion primitive and a hardware-free test for it.

Problem

drmtap_convert_format()'s AR30/XR30 path is a naive >>2 bit-shift. That is correct for SDR 10-bit, but for HDR10 it just keeps the top 8 of the 10 PQ-encoded bits — no PQ decode, no BT.2020 gamut, no tone-map — so an HDR scanout captures washed out (a 1000-nit highlight came out ~53% gray).

This change

New public drmtap_tonemap_hdr10():

  1. PQ (SMPTE ST 2084) EOTF -> linear light (nits)
  2. BT.2020 -> BT.709 gamut (linear)
  3. highlight-preserving tone-map to the SDR range (diffuse white = 203 nits per BT.2408; highlights roll off toward white)
  4. sRGB OETF -> 8-bit

PQ and sRGB are table-driven (built once via pthread_once); only the 3x3 matrix and the tone curve run per pixel. libm is wired into meson and the crate build.rs.

Test (no HDR hardware)

test_hdr.c PQ-encodes known luminances and checks the 8-bit curve:

nits -> 8-bit:  5=43   100=186   203(SDR white)=250   600=255   1000=255

Monotonic, dark stays dark, SDR white maps bright (250) instead of crushed, highlights reach white.

Scope / what's left on #16

This is the conversion primitive only. Still open on #16:

  • Decide when to tone-map (HDR vs plain SDR 10-bit) from the connector Colorspace / HDR_OUTPUT_METADATA — the pixel format alone doesn't say (XR30/AR30 is used for SDR 10-bit too), so applying this unconditionally would wrongly tone-map SDR. Needs helper-side metadata reading + new wire fields.
  • Wire it into gpu_auto_process, and make the highlight roll-off peak-aware using the metadata peak luminance.
  • The EGL readback path (currently 8-bit glReadPixels) for tiled HDR scanouts.
  • XR48/AR48 (16-bit) and P010 (10-bit YUV) formats.

Validation

release+werror, debug+asan+ubsan, buildtype=plain+werror, meson test 6/6 (incl. new hdr), cppcheck clean, cargo build/test clean (sys + wrapper, clean rebuild confirms the libm link).

Part of #16

First piece of #16. drmtap_convert_format's AR30/XR30 path is a naive >>2
bit-shift: fine for SDR 10-bit, but for HDR10 it just keeps the top 8 of the
10 PQ-encoded bits with no PQ decode / gamut / tone-map, so an HDR scanout
captures washed out (a 1000-nit highlight came out ~53% gray).

Add drmtap_tonemap_hdr10(): PQ (SMPTE ST 2084) EOTF to linear light,
BT.2020 -> BT.709 gamut, a highlight-preserving tone-map to the SDR range
(diffuse white = 203 nits per BT.2408, highlights rolled off toward white),
then the sRGB OETF back to 8-bit. PQ and sRGB are table-driven (built once via
pthread_once); only the 3x3 gamut matrix and the tone curve run per pixel.

test_hdr.c validates the curve without HDR hardware: it PQ-encodes known
luminances and checks the 8-bit result is monotonic, dark stays dark, SDR
white maps bright (250, not crushed), and highlights reach white.

This is the conversion primitive only. Deciding when to apply it (HDR vs plain
SDR 10-bit, from the connector Colorspace / HDR_OUTPUT_METADATA, since the
pixel format alone does not say) and wiring it into the capture pipeline is
the next step on #16; the EGL readback path and the XR48/AR48/P010 formats
also remain.

Part of #16
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added HDR10 (PQ, BT.2020) to SDR (8-bit XRGB8888) tone-mapping for supported HDR input formats.
    • Exposed a new public drmtap_tonemap_hdr10() API for converting HDR frames into SDR images.
  • Tests

    • Added an HDR-focused unit test validating tone-mapping monotonicity, correct handling of dark/white levels, highlight roll-off behavior, and expected error codes for unsupported formats and invalid arguments.

Walkthrough

This PR adds a public HDR10 tone-mapping API, implements PQ/BT.2020 to SDR conversion in the C library and Rust sys crate copy, links libm in the build scripts, and adds a Meson-registered HDR unit test.

Changes

HDR10 tone mapping

Layer / File(s) Summary
Public HDR10 API contract
include/drmtap.h, bindings/rust/libdrmtap-sys/csrc/drmtap.h
drmtap_tonemap_hdr10() is declared in both headers with HDR10-to-SDR parameter, format, and return-value documentation.
Tone-mapping implementation and math linkage
src/pixel_convert.c, bindings/rust/libdrmtap-sys/csrc/pixel_convert.c, bindings/rust/libdrmtap-sys/build.rs, meson.build
The C implementation adds HDR10 lookup tables, soft-knee tone mapping, and the exported conversion entry point, while the build scripts link libm for the math routines.
HDR unit test wiring and curve checks
meson.build, tests/test_hdr.c
Meson adds the test_hdr executable, and the new test checks curve monotonicity, brightness thresholds, and unsupported-format and invalid-argument errors.

Sequence Diagram(s)

sequenceDiagram
  participant Test as "tests/test_hdr.c"
  participant ToneMap as "drmtap_tonemap_hdr10()"
  participant Once as "pthread_once"
  participant Init as "hdr_lut_init()"
  participant Pixel as "tonemap_ar30_pixel"
  Test->>ToneMap: drmtap_tonemap_hdr10(src, dst, ...)
  ToneMap->>Once: initialize LUTs once
  Once->>Init: fill PQ and sRGB tables
  ToneMap->>Pixel: tonemap each AR30/XR30 pixel
  ToneMap-->>Test: 0 or error code
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Poem

🐇 Hoppity hop, I tuned the glow,
from PQ carrots down to SDR snow.
With a soft-knee twitch and a whisker of math,
bright highlights pranced along the path.
The burrow now hums: "0" on success! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: an HDR10-to-SDR tone-mapping primitive with tests.
Description check ✅ Passed The description is directly related to the change set and accurately describes the new tone-mapping primitive and test.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/hdr-tonemap

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bindings/rust/libdrmtap-sys/csrc/pixel_convert.c`:
- Around line 424-441: The row-stride checks in drmtap_tonemap_hdr10 are missing
in this Rust sys copy, so callers can pass src_stride or dst_stride smaller than
width * 4 and trigger out-of-bounds access when the per-row loop indexes 32-bit
pixels. Add the same validation used elsewhere in pixel_convert.c before
pthread_once/hdr_lut_init and the row loop, rejecting inputs where either stride
is too small for the requested width, and keep the existing drmtap_tonemap_hdr10
argument checks and format gate intact.

In `@src/pixel_convert.c`:
- Around line 406-413: Rename the new channel temporaries in the pixel
conversion path to snake_case to match C style guidelines. Update the BT.2020
linear-light inputs in the conversion logic around the pixel unpacking and
matrix transform so the variables currently named Rl, Gl, and Bl use descriptive
snake_case names such as r_nits, g_nits, and b_nits, and keep the subsequent
r/g/b calculation using the renamed locals.
- Around line 424-441: The drmtap_tonemap_hdr10 routine currently only validates
pointers, dimensions, and src_format, but it still assumes each row is wide
enough for width 32-bit pixels. Add stride validation in drmtap_tonemap_hdr10
before the loop so src_stride and dst_stride must be at least width *
sizeof(uint32_t), and return -EINVAL when either row stride is too small to hold
a full row; use the existing drmtap_tonemap_hdr10 symbol and the row access
through s[x]/d[x] as the place to enforce this check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dfa4b31c-4158-4ede-b1c7-f5376782b5ec

📥 Commits

Reviewing files that changed from the base of the PR and between 4ea9768 and 2efda9e.

📒 Files selected for processing (7)
  • bindings/rust/libdrmtap-sys/build.rs
  • bindings/rust/libdrmtap-sys/csrc/drmtap.h
  • bindings/rust/libdrmtap-sys/csrc/pixel_convert.c
  • include/drmtap.h
  • meson.build
  • src/pixel_convert.c
  • tests/test_hdr.c
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{c,h}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{c,h}: Keep lines to a 100-character soft limit and 120-character hard limit.
Use // for single-line comments and /* */ for multi-line comments.
Always check malloc returns and free resources in cleanup paths.

Files:

  • tests/test_hdr.c
  • include/drmtap.h
  • bindings/rust/libdrmtap-sys/csrc/drmtap.h
  • src/pixel_convert.c
  • bindings/rust/libdrmtap-sys/csrc/pixel_convert.c
tests/**/*.c

📄 CodeRabbit inference engine (AGENTS.md)

tests/**/*.c: Write tests using simple assert-based validation without an external test framework.
Use DRM_DEVICE from the environment in integration tests instead of hardcoding /dev/dri/card0.

Files:

  • tests/test_hdr.c
**/*.h

📄 CodeRabbit inference engine (AGENTS.md)

**/*.h: Use 4-space indentation, never tabs, and 1TBS brace style in C header files.
Name C functions and variables in snake_case, with public API functions prefixed drmtap_.
Name macros and constants in UPPER_SNAKE_CASE, prefixed DRMTAP_ when project-specific.
Use snake_case_t for internal types and drmtap_* for public types.
Use #ifndef DRMTAP_MODULE_H / #define DRMTAP_MODULE_H style header guards.
Public API functions in include/drmtap.h must use Doxygen comments describing purpose, parameters, return values, and error codes.

Files:

  • include/drmtap.h
  • bindings/rust/libdrmtap-sys/csrc/drmtap.h
src/**/*.c

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.c: Use 4-space indentation, never tabs, and 1TBS brace style in C source files.
Name C functions and variables in snake_case, with public API functions prefixed drmtap_.
Name macros and constants in UPPER_SNAKE_CASE, prefixed DRMTAP_ when project-specific.
Use snake_case_t for internal types and drmtap_* for public types.
Return negative errno values for errors and never abort.
Every .c file must start with the standard libdrmtap copyright/license header block, followed by a Doxygen @file comment.
Organize C source files as includes first, then private types/constants, then static functions, then public functions in header-declaration order.
Use simple // comments for internal static functions instead of full Doxygen blocks.

Files:

  • src/pixel_convert.c
🔇 Additional comments (7)
include/drmtap.h (1)

320-345: LGTM!

bindings/rust/libdrmtap-sys/csrc/drmtap.h (1)

320-345: LGTM!

meson.build (1)

62-64: LGTM!

Also applies to: 78-78, 211-221

bindings/rust/libdrmtap-sys/build.rs (1)

62-63: LGTM!

src/pixel_convert.c (1)

28-29: LGTM!

Also applies to: 329-405, 414-421, 436-450

bindings/rust/libdrmtap-sys/csrc/pixel_convert.c (1)

28-29: LGTM!

Also applies to: 329-423, 436-450

tests/test_hdr.c (1)

1-101: LGTM!

Comment on lines +424 to +441
int drmtap_tonemap_hdr10(const void *src, void *dst,
uint32_t width, uint32_t height,
uint32_t src_stride, uint32_t dst_stride,
uint32_t src_format) {
if (!src || !dst || width == 0 || height == 0) {
return -EINVAL;
}
/* XR30 = XRGB2101010 ('XR30'), AR30 = ARGB2101010 ('AR30'). */
if (src_format != 0x30335258u && src_format != 0x30335241u) {
return -ENOTSUP;
}

pthread_once(&g_hdr_once, hdr_lut_init);

for (uint32_t y = 0; y < height; y++) {
const uint32_t *s = (const uint32_t *)
((const uint8_t *)src + (size_t)y * src_stride);
uint32_t *d = (uint32_t *)((uint8_t *)dst + (size_t)y * dst_stride);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Mirror the row-stride validation in the Rust sys copy.

This copy also indexes width 32-bit pixels per row without ensuring src_stride and dst_stride are at least width * 4, so invalid caller strides can overread or overwrite memory.

Proposed validation
     if (src_format != 0x30335258u && src_format != 0x30335241u) {
         return -ENOTSUP;
     }
 
+    size_t row_bytes = (size_t)width * sizeof(uint32_t);
+    if (row_bytes > UINT32_MAX ||
+        src_stride < row_bytes ||
+        dst_stride < row_bytes) {
+        return -EINVAL;
+    }
+
     pthread_once(&g_hdr_once, hdr_lut_init);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
int drmtap_tonemap_hdr10(const void *src, void *dst,
uint32_t width, uint32_t height,
uint32_t src_stride, uint32_t dst_stride,
uint32_t src_format) {
if (!src || !dst || width == 0 || height == 0) {
return -EINVAL;
}
/* XR30 = XRGB2101010 ('XR30'), AR30 = ARGB2101010 ('AR30'). */
if (src_format != 0x30335258u && src_format != 0x30335241u) {
return -ENOTSUP;
}
pthread_once(&g_hdr_once, hdr_lut_init);
for (uint32_t y = 0; y < height; y++) {
const uint32_t *s = (const uint32_t *)
((const uint8_t *)src + (size_t)y * src_stride);
uint32_t *d = (uint32_t *)((uint8_t *)dst + (size_t)y * dst_stride);
int drmtap_tonemap_hdr10(const void *src, void *dst,
uint32_t width, uint32_t height,
uint32_t src_stride, uint32_t dst_stride,
uint32_t src_format) {
if (!src || !dst || width == 0 || height == 0) {
return -EINVAL;
}
/* XR30 = XRGB2101010 ('XR30'), AR30 = ARGB2101010 ('AR30'). */
if (src_format != 0x30335258u && src_format != 0x30335241u) {
return -ENOTSUP;
}
size_t row_bytes = (size_t)width * sizeof(uint32_t);
if (row_bytes > UINT32_MAX ||
src_stride < row_bytes ||
dst_stride < row_bytes) {
return -EINVAL;
}
pthread_once(&g_hdr_once, hdr_lut_init);
for (uint32_t y = 0; y < height; y++) {
const uint32_t *s = (const uint32_t *)
((const uint8_t *)src + (size_t)y * src_stride);
uint32_t *d = (uint32_t *)((uint8_t *)dst + (size_t)y * dst_stride);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindings/rust/libdrmtap-sys/csrc/pixel_convert.c` around lines 424 - 441, The
row-stride checks in drmtap_tonemap_hdr10 are missing in this Rust sys copy, so
callers can pass src_stride or dst_stride smaller than width * 4 and trigger
out-of-bounds access when the per-row loop indexes 32-bit pixels. Add the same
validation used elsewhere in pixel_convert.c before pthread_once/hdr_lut_init
and the row loop, rejecting inputs where either stride is too small for the
requested width, and keep the existing drmtap_tonemap_hdr10 argument checks and
format gate intact.

Comment thread src/pixel_convert.c Outdated
Comment on lines +406 to +413
double Rl = g_pq_lut[(pixel >> 20) & 0x3FF]; /* BT.2020 linear nits */
double Gl = g_pq_lut[(pixel >> 10) & 0x3FF];
double Bl = g_pq_lut[(pixel) & 0x3FF];

/* BT.2020 -> BT.709 in linear light. */
double r = 1.660491 * Rl - 0.587641 * Gl - 0.072850 * Bl;
double g = -0.124550 * Rl + 1.132900 * Gl - 0.008349 * Bl;
double b = -0.018151 * Rl - 0.100579 * Gl + 1.118730 * Bl;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Rename the channel temporaries to snake_case.

Rl, Gl, and Bl are new C variables in src/**/*.c; use names like r_nits, g_nits, and b_nits.

Proposed rename
-    double Rl = g_pq_lut[(pixel >> 20) & 0x3FF];  /* BT.2020 linear nits */
-    double Gl = g_pq_lut[(pixel >> 10) & 0x3FF];
-    double Bl = g_pq_lut[(pixel)       & 0x3FF];
+    double r_nits = g_pq_lut[(pixel >> 20) & 0x3FF];  /* BT.2020 linear nits */
+    double g_nits = g_pq_lut[(pixel >> 10) & 0x3FF];
+    double b_nits = g_pq_lut[(pixel)       & 0x3FF];
 
     /* BT.2020 -> BT.709 in linear light. */
-    double r =  1.660491 * Rl - 0.587641 * Gl - 0.072850 * Bl;
-    double g = -0.124550 * Rl + 1.132900 * Gl - 0.008349 * Bl;
-    double b = -0.018151 * Rl - 0.100579 * Gl + 1.118730 * Bl;
+    double r =  1.660491 * r_nits - 0.587641 * g_nits - 0.072850 * b_nits;
+    double g = -0.124550 * r_nits + 1.132900 * g_nits - 0.008349 * b_nits;
+    double b = -0.018151 * r_nits - 0.100579 * g_nits + 1.118730 * b_nits;

As per coding guidelines, src/**/*.c: “Name C functions and variables in snake_case.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
double Rl = g_pq_lut[(pixel >> 20) & 0x3FF]; /* BT.2020 linear nits */
double Gl = g_pq_lut[(pixel >> 10) & 0x3FF];
double Bl = g_pq_lut[(pixel) & 0x3FF];
/* BT.2020 -> BT.709 in linear light. */
double r = 1.660491 * Rl - 0.587641 * Gl - 0.072850 * Bl;
double g = -0.124550 * Rl + 1.132900 * Gl - 0.008349 * Bl;
double b = -0.018151 * Rl - 0.100579 * Gl + 1.118730 * Bl;
double r_nits = g_pq_lut[(pixel >> 20) & 0x3FF]; /* BT.2020 linear nits */
double g_nits = g_pq_lut[(pixel >> 10) & 0x3FF];
double b_nits = g_pq_lut[(pixel) & 0x3FF];
/* BT.2020 -> BT.709 in linear light. */
double r = 1.660491 * r_nits - 0.587641 * g_nits - 0.072850 * b_nits;
double g = -0.124550 * r_nits + 1.132900 * g_nits - 0.008349 * b_nits;
double b = -0.018151 * r_nits - 0.100579 * g_nits + 1.118730 * b_nits;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pixel_convert.c` around lines 406 - 413, Rename the new channel
temporaries in the pixel conversion path to snake_case to match C style
guidelines. Update the BT.2020 linear-light inputs in the conversion logic
around the pixel unpacking and matrix transform so the variables currently named
Rl, Gl, and Bl use descriptive snake_case names such as r_nits, g_nits, and
b_nits, and keep the subsequent r/g/b calculation using the renamed locals.

Source: Coding guidelines

Comment thread src/pixel_convert.c
Comment on lines +424 to +441
int drmtap_tonemap_hdr10(const void *src, void *dst,
uint32_t width, uint32_t height,
uint32_t src_stride, uint32_t dst_stride,
uint32_t src_format) {
if (!src || !dst || width == 0 || height == 0) {
return -EINVAL;
}
/* XR30 = XRGB2101010 ('XR30'), AR30 = ARGB2101010 ('AR30'). */
if (src_format != 0x30335258u && src_format != 0x30335241u) {
return -ENOTSUP;
}

pthread_once(&g_hdr_once, hdr_lut_init);

for (uint32_t y = 0; y < height; y++) {
const uint32_t *s = (const uint32_t *)
((const uint8_t *)src + (size_t)y * src_stride);
uint32_t *d = (uint32_t *)((uint8_t *)dst + (size_t)y * dst_stride);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject strides that cannot hold a full row.

Line 444 reads s[x] and Line 445 writes d[x] for width 32-bit pixels, but src_stride/dst_stride can be smaller than width * 4, causing row overreads or overwrites instead of returning -EINVAL.

Proposed validation
     if (src_format != 0x30335258u && src_format != 0x30335241u) {
         return -ENOTSUP;
     }
 
+    size_t row_bytes = (size_t)width * sizeof(uint32_t);
+    if (row_bytes > UINT32_MAX ||
+        src_stride < row_bytes ||
+        dst_stride < row_bytes) {
+        return -EINVAL;
+    }
+
     pthread_once(&g_hdr_once, hdr_lut_init);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
int drmtap_tonemap_hdr10(const void *src, void *dst,
uint32_t width, uint32_t height,
uint32_t src_stride, uint32_t dst_stride,
uint32_t src_format) {
if (!src || !dst || width == 0 || height == 0) {
return -EINVAL;
}
/* XR30 = XRGB2101010 ('XR30'), AR30 = ARGB2101010 ('AR30'). */
if (src_format != 0x30335258u && src_format != 0x30335241u) {
return -ENOTSUP;
}
pthread_once(&g_hdr_once, hdr_lut_init);
for (uint32_t y = 0; y < height; y++) {
const uint32_t *s = (const uint32_t *)
((const uint8_t *)src + (size_t)y * src_stride);
uint32_t *d = (uint32_t *)((uint8_t *)dst + (size_t)y * dst_stride);
int drmtap_tonemap_hdr10(const void *src, void *dst,
uint32_t width, uint32_t height,
uint32_t src_stride, uint32_t dst_stride,
uint32_t src_format) {
if (!src || !dst || width == 0 || height == 0) {
return -EINVAL;
}
/* XR30 = XRGB2101010 ('XR30'), AR30 = ARGB2101010 ('AR30'). */
if (src_format != 0x30335258u && src_format != 0x30335241u) {
return -ENOTSUP;
}
size_t row_bytes = (size_t)width * sizeof(uint32_t);
if (row_bytes > UINT32_MAX || src_stride < row_bytes || dst_stride < row_bytes) {
return -EINVAL;
}
pthread_once(&g_hdr_once, hdr_lut_init);
for (uint32_t y = 0; y < height; y++) {
const uint32_t *s = (const uint32_t *)
((const uint8_t *)src + (size_t)y * src_stride);
uint32_t *d = (uint32_t *)((uint8_t *)dst + (size_t)y * dst_stride);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pixel_convert.c` around lines 424 - 441, The drmtap_tonemap_hdr10 routine
currently only validates pointers, dimensions, and src_format, but it still
assumes each row is wide enough for width 32-bit pixels. Add stride validation
in drmtap_tonemap_hdr10 before the loop so src_stride and dst_stride must be at
least width * sizeof(uint32_t), and return -EINVAL when either row stride is too
small to hold a full row; use the existing drmtap_tonemap_hdr10 symbol and the
row access through s[x]/d[x] as the place to enforce this check.

Review feedback. drmtap_tonemap_hdr10() now rejects src/dst strides that
cannot hold a full width*4 row (-EINVAL), matching the guards the other
conversions use, so a bogus caller stride can't over-read/over-write. Added a
test case for it. Renamed the per-pixel linear channel temporaries to
snake_case (r_lin/g_lin/b_lin).
@fxd0h

fxd0h commented Jun 26, 2026

Copy link
Copy Markdown
Owner Author

Addressed all three:

  • Stride validation: drmtap_tonemap_hdr10() now rejects src/dst strides below width4 (and width4 overflowing uint32) with -EINVAL, before any indexing. Same guard the other conversions use. Added a test case (2px row with a 4-byte stride -> -EINVAL).
  • Mirror: applied to the csrc copy too (diff -q clean).
  • Renamed the linear channel temporaries Rl/Gl/Bl -> r_lin/g_lin/b_lin (snake_case).

meson test 6/6, asan clean, cppcheck clean.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bindings/rust/libdrmtap-sys/csrc/pixel_convert.c`:
- Around line 437-439: The row size calculation in pixel_convert.c has the same
wrapped-`row_bytes` risk as the pre-multiply path in src/pixel_convert.c, so
keep the guards in sync by validating `width` before computing `row_bytes` in
the conversion logic around the stride checks. Update the relevant
pre-multiply/row-bytes guard near the existing `row_bytes`, `src_stride`, and
`dst_stride` validation so the overflow protection happens before the
multiplication and matches the behavior of the source implementation.

In `@src/pixel_convert.c`:
- Around line 437-439: The stride validation in the pixel conversion path is
checking for overflow only after computing row_bytes, which can wrap first on
narrower targets. Update the width handling in the row size calculation near the
row_bytes check so the overflow is validated before multiplying, and then use
that safely computed value in the existing src_stride and dst_stride checks
inside the pixel conversion logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c169c82d-d326-41da-a703-167a5380f550

📥 Commits

Reviewing files that changed from the base of the PR and between 2efda9e and 9224c8c.

📒 Files selected for processing (3)
  • bindings/rust/libdrmtap-sys/csrc/pixel_convert.c
  • src/pixel_convert.c
  • tests/test_hdr.c
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.c

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.c: Use 4-space indentation, never tabs, and 1TBS brace style in C source files.
Name C functions and variables in snake_case, with public API functions prefixed drmtap_.
Name macros and constants in UPPER_SNAKE_CASE, prefixed DRMTAP_ when project-specific.
Use snake_case_t for internal types and drmtap_* for public types.
Return negative errno values for errors and never abort.
Every .c file must start with the standard libdrmtap copyright/license header block, followed by a Doxygen @file comment.
Organize C source files as includes first, then private types/constants, then static functions, then public functions in header-declaration order.
Use simple // comments for internal static functions instead of full Doxygen blocks.

Files:

  • src/pixel_convert.c
**/*.{c,h}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{c,h}: Keep lines to a 100-character soft limit and 120-character hard limit.
Use // for single-line comments and /* */ for multi-line comments.
Always check malloc returns and free resources in cleanup paths.

Files:

  • src/pixel_convert.c
  • tests/test_hdr.c
  • bindings/rust/libdrmtap-sys/csrc/pixel_convert.c
tests/**/*.c

📄 CodeRabbit inference engine (AGENTS.md)

tests/**/*.c: Write tests using simple assert-based validation without an external test framework.
Use DRM_DEVICE from the environment in integration tests instead of hardcoding /dev/dri/card0.

Files:

  • tests/test_hdr.c
🔇 Additional comments (3)
src/pixel_convert.c (1)

406-413: LGTM!

bindings/rust/libdrmtap-sys/csrc/pixel_convert.c (1)

406-413: LGTM!

tests/test_hdr.c (1)

92-96: LGTM!

Comment on lines +437 to +439
size_t row_bytes = (size_t)width * 4u;
if (row_bytes > UINT32_MAX || src_stride < row_bytes ||
dst_stride < row_bytes) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Mirror the pre-multiply overflow guard here too.

This Rust sys copy has the same wrapped-row_bytes risk on 32-bit size_t; keep it in lockstep with src/pixel_convert.c by checking width before multiplying.

Proposed fix
-    size_t row_bytes = (size_t)width * 4u;
-    if (row_bytes > UINT32_MAX || src_stride < row_bytes ||
-        dst_stride < row_bytes) {
+    if (width > UINT32_MAX / sizeof(uint32_t)) {
+        return -EINVAL;
+    }
+    size_t row_bytes = (size_t)width * sizeof(uint32_t);
+    if (src_stride < row_bytes || dst_stride < row_bytes) {
         return -EINVAL;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
size_t row_bytes = (size_t)width * 4u;
if (row_bytes > UINT32_MAX || src_stride < row_bytes ||
dst_stride < row_bytes) {
if (width > UINT32_MAX / sizeof(uint32_t)) {
return -EINVAL;
}
size_t row_bytes = (size_t)width * sizeof(uint32_t);
if (src_stride < row_bytes || dst_stride < row_bytes) {
return -EINVAL;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindings/rust/libdrmtap-sys/csrc/pixel_convert.c` around lines 437 - 439, The
row size calculation in pixel_convert.c has the same wrapped-`row_bytes` risk as
the pre-multiply path in src/pixel_convert.c, so keep the guards in sync by
validating `width` before computing `row_bytes` in the conversion logic around
the stride checks. Update the relevant pre-multiply/row-bytes guard near the
existing `row_bytes`, `src_stride`, and `dst_stride` validation so the overflow
protection happens before the multiplication and matches the behavior of the
source implementation.

Comment thread src/pixel_convert.c
Comment on lines +437 to +439
size_t row_bytes = (size_t)width * 4u;
if (row_bytes > UINT32_MAX || src_stride < row_bytes ||
dst_stride < row_bytes) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Check width overflow before multiplying.

row_bytes > UINT32_MAX only catches overflow after the multiply if size_t is wider than uint32_t; on 32-bit targets the (size_t)width * 4u expression can wrap first and let an invalid stride through.

Proposed fix
-    size_t row_bytes = (size_t)width * 4u;
-    if (row_bytes > UINT32_MAX || src_stride < row_bytes ||
-        dst_stride < row_bytes) {
+    if (width > UINT32_MAX / sizeof(uint32_t)) {
+        return -EINVAL;
+    }
+    size_t row_bytes = (size_t)width * sizeof(uint32_t);
+    if (src_stride < row_bytes || dst_stride < row_bytes) {
         return -EINVAL;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
size_t row_bytes = (size_t)width * 4u;
if (row_bytes > UINT32_MAX || src_stride < row_bytes ||
dst_stride < row_bytes) {
if (width > UINT32_MAX / sizeof(uint32_t)) {
return -EINVAL;
}
size_t row_bytes = (size_t)width * sizeof(uint32_t);
if (src_stride < row_bytes || dst_stride < row_bytes) {
return -EINVAL;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pixel_convert.c` around lines 437 - 439, The stride validation in the
pixel conversion path is checking for overflow only after computing row_bytes,
which can wrap first on narrower targets. Update the width handling in the row
size calculation near the row_bytes check so the overflow is validated before
multiplying, and then use that safely computed value in the existing src_stride
and dst_stride checks inside the pixel conversion logic.

@fxd0h

fxd0h commented Jun 26, 2026

Copy link
Copy Markdown
Owner Author

Note: the stride guard re-flagged here is already in place (src/pixel_convert.c lines 435-439, and the csrc mirror) — it's the exact check from the previous round, with a test case. The bot just re-anchored the thread. Merging.

@fxd0h fxd0h merged commit 0531c13 into main Jun 26, 2026
9 checks passed
@fxd0h fxd0h deleted the feat/hdr-tonemap branch June 26, 2026 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant