This is an automated email from the git hooks/post-receive script. Git pushed a commit to branch master in repository ffmpeg.
commit d8eb6779c3ba0713dc856430b71c816f5191fe22 Author: Philip Langdale <[email protected]> AuthorDate: Fri Jul 31 15:59:10 2026 +0800 Commit: Philip Langdale <[email protected]> CommitDate: Fri Aug 7 08:45:59 2026 -0700 avfilter: add truehdr_drv_cuda, the driver RTX TrueHDR network The inverse tone mapping the NVIDIA driver itself runs (the DXVA/PPE plugin nvaihdrx.dll, ppe/features/TrueHDR), as opposed to the NGX SDK snippet that truehdr_cuda drives. Like it, the output resolution equals the input and the neural core runs at ff_rtx_nn_dims' internal resolution; unlike it, the graph carries two internal surfaces -- postprocessing to debanding, and debanding to drtm -- each a float32 array exposed to its producer as a surface and to its consumer as a texture. The tone curve comes entirely out of the final drtm launch's argument block. The captured template runs ToneMapMode 0, a near-linear bypass that reads only MaxLuminance; it is byte-exact against the driver but blows out midtones, so tonemap=1 -- the default -- enables the driver's adaptive inverse tone map, which also consumes the live per-frame calculate_pov scene statistic and is shaped by the tunable floats. This network is considerably brighter than the SDK one, so preset=sdk names a tunable set that pulls its curve onto truehdr_cuda's for a like-for-like comparison. --- configure | 1 + doc/filters.texi | 75 ++++++ libavfilter/Makefile | 1 + libavfilter/allfilters.c | 1 + libavfilter/vf_truehdr_drv_cuda.c | 464 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 542 insertions(+) diff --git a/configure b/configure index f29c29e5d2..6b3ca593be 100755 --- a/configure +++ b/configure @@ -4308,6 +4308,7 @@ transpose_vaapi_filter_deps="vaapi VAProcPipelineCaps_rotation_flags" transpose_vt_filter_deps="videotoolbox VTPixelRotationSessionCreate" transpose_vulkan_filter_deps="vulkan spirv_compiler" truehdr_cuda_filter_deps="ffnvcodec nvfdata_truehdr" +truehdr_drv_cuda_filter_deps="ffnvcodec nvfdata_truehdr_drv" unsharp_opencl_filter_deps="opencl" uspp_filter_deps="gpl avcodec" v360_vulkan_filter_deps="vulkan spirv_compiler" diff --git a/doc/filters.texi b/doc/filters.texi index 2dc7202da2..2f4a8098d2 100644 --- a/doc/filters.texi +++ b/doc/filters.texi @@ -27930,6 +27930,81 @@ are @emph{not} shipped: the filter is only built when an @code{nvidia-video-filters} package carrying the TrueHDR data is installed, and @option{data} defaults to that package's data directory. +@section truehdr_drv_cuda + +Expand SDR to HDR with the NVIDIA driver's TrueHDR network, running it directly +on CUDA. + +This is the driver's own inverse tone mapping, not the NGX SDK network that +@ref{truehdr_cuda} drives. As there, the output resolution equals the input. +The curve is much brighter than the SDK one, which is what @option{preset} and +the tunables exist to manage. + +It accepts the following options: + +@table @option +@item tonemap +Tone-map algorithm: @code{0} is the captured bypass, a near-linear curve that +reads only @option{maxluminance}; @code{1} is the driver's adaptive inverse tone +map, which additionally consumes a live per-frame scene statistic and is shaped +by the tunables below. @code{-1}, the default, is auto and resolves to +@code{1}. @code{0} is byte-exact against the driver but blows out midtones, so +it is mostly of interest for verification. + +@item preset +A named set of tunables. @code{none} (the default) uses them as given; +@code{sdk} pulls this network's curve onto @ref{truehdr_cuda}'s, for a +like-for-like comparison between the two. A preset only fills in the tunables +left at auto (@code{-1}), so anything passed explicitly alongside it wins. + +@item maxluminance +Peak luminance of the target display in nits, @code{400} to @code{2000}. +Default @code{1000}. The only tunable @code{tonemap=0} reads. + +@item contrast +Adaptive tone-curve contrast, @code{0.1} to @code{4}. Default @code{1}. + +@item saturation +Adaptive chroma saturation, @code{0} to @code{4}. Default @code{1} (neutral). + +@item middlegray +Adaptive middle-gray reference, @code{1} to @code{100}. Default @code{-1}, +auto: @code{50}, or the value @option{preset} names. As on @ref{truehdr_cuda} +this is the main exposure lever. + +@item exposure +Highlight-expansion gain, @code{1} to @code{1000}. Default @code{-1}, auto: +@code{200}, or the value @option{preset} names. + +@item gamma +Per-channel output gamma, @code{0.25} to @code{4}. Default @code{1} (off). + +@item format +Output pixel format: @code{rgbaf16le} (the default) or @code{x2bgr10le}. These +are two different colour spaces; see @ref{truehdr_cuda} for what each one means. + +@item gamut +For @code{x2bgr10le} output: @code{1} (the default) converts to Rec.2020 +primaries for true HDR10, @code{0} keeps Rec.709. + +@item data +Directory holding the extracted cubins and @file{weights.bin}. + +@item experimental_arch +Allow GPU architectures whose cubins were matched statically rather than +exercised. Ada (sm_89) and Blackwell do not need this. +@end table + +@subsection Supported formats + +Input is 8-bit R-first packed RGB, @code{rgb0} or @code{rgba}. Output is +@code{rgbaf16le} or @code{x2bgr10le} as selected by @option{format}. + +The cubins and weights are extracted from the proprietary NVIDIA libraries and +are @emph{not} shipped: the filter is only built when an +@code{nvidia-video-filters} package carrying the driver TrueHDR data is installed, and +@option{data} defaults to that package's data directory. + @anchor{vsr_cuda} @section vsr_cuda diff --git a/libavfilter/Makefile b/libavfilter/Makefile index c26bee04ff..1222b87eec 100644 --- a/libavfilter/Makefile +++ b/libavfilter/Makefile @@ -554,6 +554,7 @@ OBJS-$(CONFIG_TRANSPOSE_VT_FILTER) += vf_transpose_vt.o OBJS-$(CONFIG_TRANSPOSE_VULKAN_FILTER) += vf_transpose_vulkan.o vulkan.o vulkan_filter.o OBJS-$(CONFIG_TRIM_FILTER) += trim.o OBJS-$(CONFIG_TRUEHDR_CUDA_FILTER) += vf_truehdr_cuda.o rtx_cuda.o +OBJS-$(CONFIG_TRUEHDR_DRV_CUDA_FILTER) += vf_truehdr_drv_cuda.o rtx_cuda.o OBJS-$(CONFIG_UNPREMULTIPLY_FILTER) += vf_premultiply.o framesync.o OBJS-$(CONFIG_UNSHARP_FILTER) += vf_unsharp.o OBJS-$(CONFIG_UNSHARP_OPENCL_FILTER) += vf_unsharp_opencl.o opencl.o \ diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c index cd8ac56ab1..6704a9cd06 100644 --- a/libavfilter/allfilters.c +++ b/libavfilter/allfilters.c @@ -522,6 +522,7 @@ extern const FFFilter ff_vf_transpose_vt; extern const FFFilter ff_vf_transpose_vulkan; extern const FFFilter ff_vf_trim; extern const FFFilter ff_vf_truehdr_cuda; +extern const FFFilter ff_vf_truehdr_drv_cuda; extern const FFFilter ff_vf_unpremultiply; extern const FFFilter ff_vf_unsharp; extern const FFFilter ff_vf_unsharp_opencl; diff --git a/libavfilter/vf_truehdr_drv_cuda.c b/libavfilter/vf_truehdr_drv_cuda.c new file mode 100644 index 0000000000..98a0e76a1f --- /dev/null +++ b/libavfilter/vf_truehdr_drv_cuda.c @@ -0,0 +1,464 @@ +/* + * Copyright (C) 2026 Philip Langdale <[email protected]> + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * @file + * Inverse tone-mapping (SDR -> HDR) filter driving the NVIDIA *driver* TrueHDR + * network (the DXVA/PPE plugin nvaihdrx.dll, ppe/features/TrueHDR) -- distinct + * from vf_truehdr_cuda, which runs the NGX SDK snippet (nvngx_truehdr.dll). The + * plugin's cubins were extracted and the forward pass reverse-engineered by + * running it on Linux via loader_ppe and intercepting the live CUDA Driver-API + * launches. truehdr_drv_cuda_gen.h encodes how the whole graph (grids, scratch + * allocations, packed arg-buffer scalars, weight-upload targets, pointer fixups) + * scales with the input W,H -- derived and validated byte-exact against the + * loader (rtx-video-re). The filter evaluates that at config time and replays + * the graph with libcuda; no DLL is needed at run time. The replay machinery + * itself is rtx_cuda.c. + * + * Like the SDK filter this does NOT rescale: output resolution == input + * resolution, with the neural core at the internal NW x NH of ff_rtx_nn_dims. + * Unlike it, the graph carries two INTERNAL surfaces -- S1 + * (postprocessing -> debanding) and S2 (debanding -> drtm) -- each a float32 + * array exposed to its producer as a surface and to its consumer as a texture. + * + * The tone curve is driven entirely from the final truehdr_drtm launch's + * argument block. The captured template runs ToneMapMode 0, a near-linear + * bypass reading only MaxLuminance (byte-exact vs the loader, but it blows out + * midtones); tonemap=1 (the default) enables the driver's adaptive + * inverse-tone-map, which additionally consumes the live per-frame + * calculate_pov scene stat and is shaped by the tunable floats. This network is + * much brighter than the SDK one, so preset=sdk names a tunable set that pulls + * its curve onto the SDK filter's. + * + * The cubins and the weights blob are external files (the "data" option), + * extracted from the proprietary driver and not shipped with FFmpeg. + */ + +#include "libavutil/common.h" +#include "libavutil/hwcontext.h" +#include "libavutil/mem.h" +#include "libavutil/opt.h" +#include "libavutil/pixdesc.h" + +#include "avfilter.h" +#include "filters.h" +#include "rtx_cuda.h" +#include "video.h" + +/* Generated by rtx-video-re from the proprietary NVIDIA library, and + * installed rather than carried here -- located, together with the cubins and + * weights it names, through pkg-config (see configure's nvfdata_* checks). */ +#include <truehdr_drv_cuda_gen.h> + +FF_RTX_ASSERT_MODULE_LAYOUT(ThdrvModule); +FF_RTX_ASSERT_FUNC_LAYOUT(ThdrvFunc); +FF_RTX_ASSERT_UPLOAD_LAYOUT(ThdrvGenUpload); +FF_RTX_ASSERT_LAUNCH_LAYOUT(ThdrvGenLaunch); + +/* Supported packed frame formats. Input is read format-agnostically through a + * texture normalized to [0,1] and there is no B-first path, so it is the shared + * table's R-first 8-bit rows -- exactly what FF_RTX_N_RGB8_R_FIRST names. + * Output: scRGB fp16 rgba (linear, drtm arg0x2c=0) or HDR10 x2bgr10le (PQ / + * SMPTE ST.2084, drtm arg0x2c=1 -> the kernel emits [0,1] PQ values that the + * SUST.P.2D packs into the 10-bit surface). See rtx-video-re docs/drtm610/. */ +static const FFRtxPixFmt thdrv_out_fmts[] = { + { AV_PIX_FMT_RGBAF16LE, CU_AD_FORMAT_HALF, 8, 0 }, + { AV_PIX_FMT_X2BGR10LE, CU_AD_FORMAT_UNORM_INT_101010_2, 4, 0 }, +}; + +/* Tone-curve presets (the `preset` option). A preset names a whole tunable set; + * "sdk" makes this (much brighter) driver network approximate the SDK truehdr_cuda + * filter's tone curve. */ +enum { + THDRV_PRESET_NONE = 0, + THDRV_PRESET_SDK = 1, +}; + +typedef struct TrueHdrDrvCudaContext { + const AVClass *class; + + FFRtxCuda r; + FFRtxImage *in_img; ///< pitched linear memory (PITCH2D texture) + FFRtxImage *out_img; + FFRtxImage *s1, *s2; ///< graph-internal surfaces + + int W, H; ///< input == output size + + const FFRtxPixFmt *inpf, *outpf; + + /* options */ + double maxluminance; + int preset; ///< tone-curve preset (THDRV_PRESET_*), applied over the tunables + int tonemap; ///< 0 = byte-exact bypass; 1 = adaptive inverse-tone-map + double contrast, saturation, middlegray, exposure, gamma; + char *out_format; ///< "rgbaf16le" (scRGB) or "x2bgr10le" (HDR10 PQ) + int gamut; ///< x2bgr10le: 1 = Rec.2020 primaries (HDR10), 0 = Rec.709 + int experimental_arch; ///< allow the unverified non-Blackwell (sm_75) path + char *data_dir; +} TrueHdrDrvCudaContext; + +#define OFFSET(x) offsetof(TrueHdrDrvCudaContext, x) +#define FLAGS (AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_FILTERING_PARAM) + +static const AVOption truehdr_drv_cuda_options[] = { + { "maxluminance", "peak luminance in nits (400..2000)", OFFSET(maxluminance), AV_OPT_TYPE_DOUBLE, {.dbl=1000}, 400, 2000, FLAGS }, + /* preset: a named tunable set. The driver network is a different, much brighter + * net than the SDK truehdr_cuda filter; preset=sdk applies values (found by + * sweeping this filter against the SDK filter on real content) that pull its + * adaptive curve onto the SDK's -- tonemap=1, exposure=800, middlegray=15 (14 for + * PQ output, which weights midtones more). It only fills in the tunables left + * at "auto" (-1), so e.g. preset=sdk:exposure=600 keeps the explicit exposure. */ + { "preset", "tone-curve preset over the tunables: none, or sdk (emulate the SDK truehdr_cuda curve)", + OFFSET(preset), AV_OPT_TYPE_INT, {.i64=THDRV_PRESET_NONE}, 0, 1, FLAGS, .unit = "preset" }, + { "none", "use the tunables directly (no preset)", 0, AV_OPT_TYPE_CONST, {.i64=THDRV_PRESET_NONE}, 0, 0, FLAGS, .unit = "preset" }, + { "sdk", "emulate the SDK truehdr_cuda tone curve", 0, AV_OPT_TYPE_CONST, {.i64=THDRV_PRESET_SDK}, 0, 0, FLAGS, .unit = "preset" }, + /* tonemap 0 = the captured ToneMapMode-0 bypass (near-linear, reads only + * MaxLuminance) -- byte-exact vs loader_ppe. tonemap 1 = the driver's adaptive + * inverse-tone-map, which consumes the live per-frame calculate_pov scene stat + * (fraction of bright pixels) and is shaped by the tone floats below. The exact + * production values the driver's DXVA/PPE config passes are not observable from + * the loader (we are the caller), so these are exposed with neutral defaults; + * the curve rolls highlights off toward, not to, the peak (an inverse-tone-map, + * not a linear scale). Named by live value-correlation (rtx-video-re + * docs/drtm610/). */ + /* The three tunables a preset names default to -1 = auto: the preset's value + * if one is selected, else the neutral one quoted in the help text. A + * sentinel rather than the neutral value itself, so that passing that value + * explicitly is distinguishable from not passing it at all. */ + { "tonemap", "tone-map algorithm: -1=auto (1), 0=bypass (byte-exact), 1=adaptive inverse-tone-map", + OFFSET(tonemap), AV_OPT_TYPE_INT, {.i64=-1}, -1, 1, FLAGS }, + { "contrast", "adaptive tone-curve contrast (tonemap=1)", OFFSET(contrast), AV_OPT_TYPE_DOUBLE, {.dbl=1.0}, 0.1, 4.0, FLAGS }, + { "saturation", "adaptive chroma saturation (tonemap=1; 1=neutral)", OFFSET(saturation), AV_OPT_TYPE_DOUBLE, {.dbl=1.0}, 0.0, 4.0, FLAGS }, + { "middlegray", "adaptive tone-curve middle-gray reference (tonemap=1; -1=auto, neutral 50)", OFFSET(middlegray), AV_OPT_TYPE_DOUBLE, {.dbl=-1.0}, -1.0, 100.0, FLAGS }, + { "exposure", "adaptive highlight-expansion gain (tonemap=1; -1=auto, neutral 200)", OFFSET(exposure), AV_OPT_TYPE_DOUBLE, {.dbl=-1.0}, -1.0, 1000.0,FLAGS }, + /* Per-channel gamma (drtm arg0x10, out = scale*ch^gamma). Inert until enabled + * by arg0x40 AND ToneMapMode 1; the filter enables it only when gamma!=1 (so the + * default leaves the mode-1 arg buffer untouched). 1.0 = identity. */ + { "gamma", "per-channel output gamma (tonemap=1; 1.0=off)", OFFSET(gamma), AV_OPT_TYPE_DOUBLE, {.dbl=1.0}, 0.25,4.0, FLAGS }, + /* Output transfer/format. rgbaf16le = scRGB (linear Rec.709, 1.0=80nit) -- + * drtm arg0x2c=0. x2bgr10le = HDR10: PQ / SMPTE ST.2084 transfer (drtm + * arg0x2c=1, kernel emits [0,1] -> 10-bit surface). */ + { "format", "output: rgbaf16le=scRGB linear 80nit (default), x2bgr10le=HDR10 PQ", + OFFSET(out_format), AV_OPT_TYPE_STRING, {.str="rgbaf16le"}, 0, 0, FLAGS }, + /* x2bgr10le only: apply the drtm Rec.709->Rec.2020 gamut matrix (arg0x2b) so + * the PQ output is standards-correct HDR10 / BT.2100. 0 keeps Rec.709 primaries + * (PQ-over-709). Ignored for the linear rgbaf16le/scRGB output (Rec.709). */ + { "gamut", "x2bgr10le: 1=Rec.2020 primaries (HDR10, default), 0=keep Rec.709", + OFFSET(gamut), AV_OPT_TYPE_BOOL, {.i64=1}, 0, 1, FLAGS }, + /* The truehdr_* glue cubins ship as multi-arch fatbins (sm_75/sm_80/sm_120/ + * sm_121; sm_80 covers all Ampere/Ada via minor-version compat). The four + * k_conv conv-layer cubins have four same-named copies per arch; + * `rtxv fatbins --cross-major` pairs them across the major boundary by three + * agreeing static signals (.text rank, ISETP output-size bounds, SASS immediate + * fingerprint), yielding sm_75/sm_86/sm_87/sm_89/sm_120 -- so Turing/Ampere/Ada + * all get k_conv images. sm_89 (Ada) is confirmed byte-identical to the DLL on + * an RTX 4060 Ti and runs ungated; the other statically-matched arches remain + * UNVERIFIED on real hardware, so this opt-in lifts the guard for those GPUs. */ + { "experimental_arch", "attempt the statically-matched but unverified non-Blackwell path (sm_89/Ada does not need this)", + OFFSET(experimental_arch), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS }, + { "data", "directory with extracted driver TrueHDR cubins + weights.bin", + OFFSET(data_dir), AV_OPT_TYPE_STRING, {.str=TRUEHDRDRV_DEFAULT_DATA_DIR}, 0, 0, FLAGS }, + { NULL } +}; + +AVFILTER_DEFINE_CLASS(truehdr_drv_cuda); + +FF_RTX_ASSERT_PRIV_LAYOUT(TrueHdrDrvCudaContext); + +/* The glue kernels carry sm_75/80/120/121 (sm_80 serves Ampere/Ada via minor + * compat); the k_conv layers carry sm_75/86/87/89/120 when built with + * `rtxv fatbins --cross-major`. So Turing/Ampere/Ada all have a full graph -- + * but that path is matched statically, not exercised on real hardware. */ +static const FFRtxArchGate thdrv_gate = { + .gate_msg = + "truehdr_drv_cuda is validated on Blackwell (cc 12.x) and Ada (cc 8.9); " + "this GPU is cc %d.%d. Its k_conv is statically matched but unverified " + "-- set experimental_arch=1 to attempt it anyway.\n", + .warn_msg = + "truehdr_drv_cuda: EXPERIMENTAL non-Blackwell (cc %d.%d) path -- k_conv is " + "statically matched (sm_75/86/87/89), UNVERIFIED on real hardware.\n", +}; + +/* ------------------------------------------------------------------------- * + * One-time graph setup for W,H. Must run with the CUDA context current. + * ------------------------------------------------------------------------- */ +static void fill_sizes(AVFilterContext *ctx, long long *sz) +{ + TrueHdrDrvCudaContext *s = ctx->priv; + int NW, NH; + ff_rtx_nn_dims(s->W, s->H, &NW, &NH); + thdrv_fill_allocs(s->W, s->H, NW, NH, sz); +} + +/* One internal graph surface (S1 or S2): a float32 array exposed to its producer + * kernel as a surface and to its consumer as a texture. The texture descriptor + * mirrors the input texture (normalized coords, linear filter) so the producer's + * pixel-coord SUST and the consumer's normalized TLD line up exactly as they do + * in loader_ppe. */ +static FFRtxImage *mk_interm(AVFilterContext *ctx, FFRtxCuda *r, int W, int Ha) +{ + return ff_rtx_image_array(ctx, r, W, Ha, CU_AD_FORMAT_FLOAT, + FF_RTX_TEX | FF_RTX_SURF | FF_RTX_LDST); +} + +static int setup_graph(AVFilterContext *ctx) +{ + TrueHdrDrvCudaContext *s = ctx->priv; + ThdrvGenUpload *up; + thdrv_devptr handle[7] = { 0 }; + int W = s->W, H = s->H, NW, NH, ret, nup; + int Ha = (H + 15) & ~15; /* conv padding rounds height to a multiple of 16 */ + int sdk, tonemap; + double exposure, middlegray; + uint8_t *a; + + ff_rtx_nn_dims(W, H, &NW, &NH); + + if ((ret = ff_rtx_arch_gate(ctx, &s->r, &thdrv_gate, s->experimental_arch)) < 0) + return ret; + if ((ret = ff_rtx_load_modules(ctx, &s->r, s->data_dir, + (const FFRtxModule *)thdrv_modules, THDRV_NMODULE, THDRV_MAX_MID, + (const FFRtxFunc *)thdrv_funcs, THDRV_NFUNC, THDRV_MAX_FID, + NULL)) < 0) + return ret; + /* Zero the arena so any scratch the conv/pov kernels read before writing is + * deterministically 0, as in a fresh loader process. */ + if ((ret = ff_rtx_alloc_arena(ctx, &s->r, THDRV_NALLOC, fill_sizes, + FF_RTX_ARENA_ZERO)) < 0) + return ret; + + up = av_calloc(THDRV_NUPLOAD, sizeof(*up)); + if (!up) + return AVERROR(ENOMEM); + nup = thdrv_fill_uploads(W, H, NW, NH, (const thdrv_devptr *)s->r.alloc, up); + ret = ff_rtx_upload_weights(ctx, &s->r, s->data_dir, "weights.bin", + (const FFRtxUpload *)up, nup); + av_freep(&up); + if (ret < 0) + return ret; + + /* Snapshot the pristine arena (weights + zeroed scratch); reset from it each + * frame so the graph always reads clean scratch regardless of host memory + * reuse. */ + if ((ret = ff_rtx_snapshot_arena(ctx, &s->r)) < 0) + return ret; + + /* Input: pitched linear memory, normalized/linear sampling (matches + * loader_ppe; the texture unit normalizes the 8-bit UNORM input to [0,1]). */ + s->in_img = ff_rtx_image_pitch(ctx, &s->r, W, H, s->inpf->cufmt, s->inpf->bpp, + FF_RTX_TEX); + /* Output array + surface (HDR fp16 rgba = scRGB, or 10-bit PQ). */ + s->out_img = ff_rtx_image_array(ctx, &s->r, W, H, s->outpf->cufmt, + FF_RTX_SURF | FF_RTX_LDST); + /* Internal surfaces S1 (postprocessing->debanding) and S2 (debanding->drtm). */ + s->s1 = mk_interm(ctx, &s->r, W, Ha); + s->s2 = mk_interm(ctx, &s->r, W, Ha); + if (!s->in_img || !s->out_img || !s->s1 || !s->s2) + return AVERROR_EXTERNAL; + + /* Build the graph. thdrv_fill_graph() is generated from the same fit as the + * tables above and assigns every field through its named thdrv_*_params + * struct. The internal S1/S2 surfaces and textures are passed by fix kind, + * which is the index the generated code reads them at. The casts are only + * `unsigned long long *` vs `uint64_t *` on LP64. */ + if ((ret = ff_rtx_alloc_launches(ctx, &s->r, THDRV_NLAUNCH, sizeof(ThdrvGenLaunch))) < 0) + return ret; + handle[3] = (thdrv_devptr)s->s1->surf; + handle[4] = (thdrv_devptr)s->s1->tex; + handle[5] = (thdrv_devptr)s->s2->surf; + handle[6] = (thdrv_devptr)s->s2->tex; + if (thdrv_fill_graph(W, H, NW, NH, (const thdrv_devptr *)s->r.alloc, + (thdrv_devptr)s->in_img->tex, (thdrv_devptr)s->out_img->surf, + handle, s->r.launches) != THDRV_NLAUNCH) { + av_log(ctx, AV_LOG_ERROR, "generated fill disagrees with the tables\n"); + return AVERROR_BUG; + } + + if (THDRV_DRTM_LAUNCH < 0 || THDRV_DRTM_LAUNCH >= s->r.nlaunch) { + av_log(ctx, AV_LOG_ERROR, "no drtm launch in graph\n"); + return AVERROR_BUG; + } + a = ff_rtx_launch_at(&s->r, THDRV_DRTM_LAUNCH)->params; + + /* Resolve the tunables the preset names, before marshalling them below. The + * SDK preset selects the adaptive path and the exposure/middlegray that + * emulate the SDK truehdr_cuda curve (middlegray one step lower for PQ + * output). Only tunables left at auto (-1) take a preset value, so an + * explicit one passed alongside the preset still wins -- including one that + * happens to equal the neutral default, which a compare-against-the-default + * test could not tell apart. The resolved values live in locals: the + * AVOption fields stay as the user set them, so a re-run of config_output + * resolves from the same starting point and av_opt_get still reports what + * was asked for. */ + sdk = s->preset == THDRV_PRESET_SDK; + tonemap = s->tonemap >= 0 ? s->tonemap : 1; + exposure = s->exposure >= 0 ? s->exposure : (sdk ? 800.0 : 200.0); + middlegray = s->middlegray >= 0 ? s->middlegray + : sdk ? (s->outpf->f == AV_PIX_FMT_X2BGR10LE ? 14.0 : 15.0) : 50.0; + if (sdk) + av_log(ctx, AV_LOG_VERBOSE, + "preset=sdk: tonemap=%d exposure=%.0f middlegray=%.0f\n", + tonemap, exposure, middlegray); + + /* drtm override: peak luminance (float32, computed in double then cast to + * bit-match the reference). */ + { + float maxlum = (float)av_clipd(s->maxluminance, 400, 2000); + memcpy(a + THDRV_OFF_MAXLUMINANCE, &maxlum, 4); + } + + /* Output format flags (drtm final SUST). arg0x2c = TRANSFER: 0 = scRGB linear + * (rgb*MaxLuminance/80, fp16); 1 = PQ / SMPTE ST.2084 -> normalized [0,1] the + * 10-bit x2bgr10le surface packs. arg0x2b = GAMUT: 1 = Rec.709->Rec.2020 primary + * matrix. x2bgr10le output enables PQ, and (by default) the gamut too == HDR10 / + * BT.2100. arg0x2b is byte 3 of a packed dword, so write a single byte. */ + if (s->outpf->f == AV_PIX_FMT_X2BGR10LE) { + int32_t pq = 1; + memcpy(a + THDRV_OFF_TRANSFER, &pq, 4); + a[THDRV_OFF_GAMUT] = s->gamut ? 1 : 0; + } + + /* Adaptive inverse-tone-map (tonemap>=1, the default). The captured drtm template + * runs ToneMapMode 0 -- a near-linear bypass that reads only MaxLuminance, so + * tonemap=0 is byte-exact vs the loader but blows out midtones. Mode 1 enables the + * driver's adaptive curve, which additionally consumes the live calculate_pov + * scene stat (drtm arg0x48 = the per-frame bright-pixel fraction; the graph + * already produces it and the arena reset zero-inits its atomic accumulator each + * frame -- see filter_frame / loader_ppe) and is gated by the tone floats. Those + * MUST be non-zero or the curve divides by zero (NaN), so write the tunable set + * with neutral shadow-lift. Offsets are the drtm610-named arg offsets. Mode 0 + * is left entirely untouched. */ + if (tonemap >= 1) { + float f_contrast = (float)av_clipd(s->contrast, 0.1, 4.0); + float f_shadowlift = 1.0f; /* neutral; curve needs it non-zero */ + float f_saturation = (float)av_clipd(s->saturation, 0.0, 4.0); + float f_middlegray = (float)av_clipd(middlegray, 1.0, 100.0); + float f_exposure = (float)av_clipd(exposure, 1.0, 1000.0); + int32_t mode = tonemap; + memcpy(a + THDRV_OFF_CONTRAST, &f_contrast, 4); + memcpy(a + THDRV_OFF_SHADOWLIFT, &f_shadowlift, 4); + memcpy(a + THDRV_OFF_SATURATION, &f_saturation, 4); + memcpy(a + THDRV_OFF_MIDDLEGRAY, &f_middlegray, 4); + memcpy(a + THDRV_OFF_EXPOSURE, &f_exposure, 4); + memcpy(a + THDRV_OFF_TONEMAPMODE, &mode, 4); + /* Per-channel gamma is a separate opt-in: it needs its enable byte + * (arg0x40) set as well as the exponent (arg0x10). Only touch them when + * the user asked for a non-identity gamma, so gamma=1.0 leaves the + * (byte-exact) mode-1 arg buffer untouched. */ + if (s->gamma != 1.0) { + float g = (float)av_clipd(s->gamma, 0.25, 4.0); + memcpy(a + THDRV_OFF_GAMMA, &g, 4); + a[THDRV_OFF_GAMMAENABLE] = 1; + } + } + + av_log(ctx, AV_LOG_INFO, + "driver TrueHDR graph ready: %dx%d (NN %dx%d) %s -> %s (%d launches, %d buffers) " + "tonemap=%s\n", + W, H, NW, NH, av_get_pix_fmt_name(s->inpf->f), av_get_pix_fmt_name(s->outpf->f), + s->r.nlaunch, s->r.nalloc, + s->outpf->f == AV_PIX_FMT_X2BGR10LE + ? (tonemap ? "adaptive/PQ" : "bypass/PQ") + : (tonemap ? "adaptive" : "bypass(byte-exact)")); + return 0; +} + +/* ------------------------------------------------------------------------- * + * Per-frame: bind the input frame as a texture, replay the graph, copy out. + * ------------------------------------------------------------------------- */ +/* The output is HDR, not the SDR the input props describe -- retag so a + * colour-managed consumer interprets it. rgbaf16le = scRGB (linear light, + * Rec.709, full range); x2bgr10le = HDR10 (PQ / SMPTE ST.2084, Rec.2020 primaries + * when the gamut matrix is on -- the standard -- else Rec.709). */ +static void retag_hdr(AVFilterContext *ctx, AVFrame *out) +{ + TrueHdrDrvCudaContext *s = ctx->priv; + int hdr10 = s->outpf->f == AV_PIX_FMT_X2BGR10LE; + + out->colorspace = AVCOL_SPC_RGB; + out->color_range = AVCOL_RANGE_JPEG; + out->color_primaries = (hdr10 && s->gamut) ? AVCOL_PRI_BT2020 : AVCOL_PRI_BT709; + out->color_trc = hdr10 ? AVCOL_TRC_SMPTE2084 : AVCOL_TRC_LINEAR; +} + +static int filter_frame(AVFilterLink *inlink, AVFrame *in) +{ + TrueHdrDrvCudaContext *s = inlink->dst->priv; + const FFRtxFrameOp op = { + .in_img = s->in_img, .iW = s->W, .iH = s->H, .ibpp = s->inpf->bpp, + .out_img = s->out_img, .oW = s->W, .oH = s->H, .obpp = s->outpf->bpp, + /* Reset the arena each frame: the DLL gets fresh memory per frame, we + * reuse one arena, and a long-running host recycles it dirty. */ + .flags = FF_RTX_OP_RESET_ARENA, + }; + + return ff_rtx_filter_frame(inlink, in, &s->r, &op, retag_hdr); +} + +static int config_output(AVFilterLink *outlink) +{ + AVFilterContext *ctx = outlink->src; + AVFilterLink *inlink = ctx->inputs[0]; + TrueHdrDrvCudaContext *s = ctx->priv; + AVHWFramesContext *in_frames_ctx; + FFRtxFormats fmts = { + .in_tbl = ff_rtx_packed_rgb_fmts, .n_in = FF_RTX_N_RGB8_R_FIRST, + .out_tbl = thdrv_out_fmts, .n_out = FF_ARRAY_ELEMS(thdrv_out_fmts), + .hint = "use rgb0/rgba in, rgbaf16le/x2bgr10le out", + }; + int ret; + + ff_rtx_free_graph(ctx, &s->r); + + fmts.out_format = s->out_format; + if ((ret = ff_rtx_config_formats(ctx, inlink, &fmts, &in_frames_ctx, + &s->inpf, &s->outpf)) < 0) + return ret; + + s->W = inlink->w; + s->H = inlink->h; + + if ((ret = ff_rtx_bind_device(ctx, &s->r, in_frames_ctx)) < 0) + return ret; + if ((ret = ff_rtx_config_hwframes(ctx, outlink, &s->r, s->W, s->H, + s->outpf->f)) < 0) + return ret; + return ff_rtx_setup(ctx, &s->r, "driver TrueHDR", setup_graph); +} + +static const AVFilterPad truehdr_drv_cuda_inputs[] = { + { .name = "default", .type = AVMEDIA_TYPE_VIDEO, .filter_frame = filter_frame }, +}; + +static const AVFilterPad truehdr_drv_cuda_outputs[] = { + { .name = "default", .type = AVMEDIA_TYPE_VIDEO, .config_props = config_output }, +}; + +const FFFilter ff_vf_truehdr_drv_cuda = { + .p.name = "truehdr_drv_cuda", + .p.description = NULL_IF_CONFIG_SMALL("NVIDIA RTX driver TrueHDR SDR-to-HDR (CUDA)"), + .p.priv_class = &truehdr_drv_cuda_class, + .priv_size = sizeof(TrueHdrDrvCudaContext), + .uninit = ff_rtx_uninit, + FILTER_INPUTS(truehdr_drv_cuda_inputs), + FILTER_OUTPUTS(truehdr_drv_cuda_outputs), + FILTER_SINGLE_PIXFMT(AV_PIX_FMT_CUDA), + .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE, +}; -- To stop receiving notification emails like this one, please contact [email protected]. _______________________________________________ ffmpeg-cvslog mailing list -- [email protected] To unsubscribe send an email to [email protected]
