HDR loading and display

This is way too complicated.  Windows not tested.
This commit is contained in:
p
2026-09-26 16:21:08 +02:00
parent 0c0263ef6d
commit 38225b9320
53 changed files with 5776 additions and 482 deletions
+1 -1
View File
@@ -569,7 +569,7 @@ if (WIN32)
dn/window-appearance-windows.cpp
)
target_link_libraries(dn PRIVATE
dwrite dwmapi gdi32 shell32 shlwapi ole32 advapi32 user32)
dwrite dwmapi dxgi gdi32 shell32 shlwapi ole32 advapi32 user32)
set(DN_TEXT_SOURCE dn/text-windows.cpp)
set(DN_TEXT_LIBRARIES dwrite)
set_target_properties(dn PROPERTIES WIN32_EXECUTABLE $<NOT:$<CONFIG:Debug>>)
+2 -1
View File
@@ -39,7 +39,8 @@ We care about
Do it yourself
~~~~~~~~~~~~~~
- HDR: the project scope is huge as it is, but you're welcome to lend a hand.
- HDR: verified to roughly work on Linux and macOS but it's not the focus.
- Right-to-left layouts.
Rejected
~~~~~~~~
+7 -1
View File
@@ -14,7 +14,13 @@
// Relocatable packages find theirs next to the executable.
#define DAWN_LOCALEDIR "@CMAKE_INSTALL_FULL_LOCALEDIR@"
#if defined __GNUC__ || defined __clang__
#if defined __MINGW32__
// Plain printf would be checked against the Microsoft runtime, rejecting
// %zu, whereas C++ gets MinGW's own stdio. This follows the one in use.
#include <_mingw.h>
#define DAWN_FORMAT(format_index, first_arg) \
__attribute__((format(__MINGW_PRINTF_FORMAT, format_index, first_arg)))
#elif defined __GNUC__ || defined __clang__
#define DAWN_FORMAT(format_index, first_arg) \
__attribute__((format(printf, format_index, first_arg)))
#else
+2
View File
@@ -171,6 +171,7 @@ constexpr ActionDef kDefs[] = {
{kToggle, {N_("_Keep Zoom and Position")}, {"pin2-symbolic"},
{{Qt::Key_K}}, {}},
{kToggle, {N_("_Colour Management")}, {"color-symbolic"}, {{Qt::Key_C}}, {}},
{kToggle, {N_("_High Dynamic Range")}, {}, {}, {}},
{kToggle, {N_("S_mooth Scaling")}, {"blend-tool-symbolic"}, {{Qt::Key_I}}, {}},
{kToggle, {N_("Highlight _Transparency")},
{"transparent-background-symbolic"}, {{Qt::Key_T}}, {}},
@@ -446,6 +447,7 @@ const MenuNode kViewerMenu[] = {
MenuNode::item(Action::Fixate),
{},
MenuNode::item(Action::ColorManagement),
MenuNode::item(Action::HighDynamicRange),
MenuNode::item(Action::Smooth),
MenuNode::item(Action::Checkerboard),
MenuNode::item(Action::NonlinearProcessing),
+1
View File
@@ -97,6 +97,7 @@ enum class Action : uint8_t {
Lock,
Fixate,
ColorManagement,
HighDynamicRange,
Smooth,
Checkerboard,
NonlinearProcessing,
+38
View File
@@ -107,4 +107,42 @@ make_display_profile_source()
return make_unique<CocoaSource>();
}
DisplayRange
macos_display_range(QScreen *screen)
{
DisplayRange range;
auto *native = screen
? screen->nativeInterface<QNativeInterface::QCocoaScreen>()
: nullptr;
NSScreen *native_screen = native ? native->nativeScreen() : nil;
if (!native_screen)
return range;
// Non-XDR MacBooks often have potential headroom too, from backlight.
range.hdr =
native_screen.maximumPotentialExtendedDynamicRangeColorComponentValue >
1;
range.headroom =
float(native_screen.maximumExtendedDynamicRangeColorComponentValue);
return range;
}
// This fires at about frame rate during an EDR ramp, so it needs no poll.
shared_ptr<void>
macos_watch_screen_parameters(function<void()> fn)
{
id observer = [[[NSNotificationCenter defaultCenter]
addObserverForName:NSApplicationDidChangeScreenParametersNotification
object:nil
queue:nil
usingBlock:^(NSNotification *) {
fn();
}] retain];
return shared_ptr<void>(static_cast<void *>(observer), [](void *p) {
id observer = static_cast<id>(p);
[[NSNotificationCenter defaultCenter] removeObserver:observer];
[observer release];
});
}
} // namespace dn
+168 -9
View File
@@ -7,6 +7,8 @@
#include "display-profile.hpp"
#include <QAbstractNativeEventFilter>
#include <QCoreApplication>
#include <QFile>
#include <QObject>
#include <QScreen>
@@ -14,8 +16,11 @@
#include <QtGui/qscreen_platform.h>
#include <QtLogging>
#include <dxgi1_6.h>
#include <windows.h>
#include <array>
#include <cwchar>
#include <functional>
#include <memory>
#include <utility>
@@ -36,20 +41,143 @@ constexpr wchar_t kUserLeaf[] =
constexpr wchar_t kUserParent[] =
L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ICM";
// The DisplayConfig target that shows the GDI device `name`.
static bool
display_config_target(
const wchar_t *name, DISPLAYCONFIG_DEVICE_INFO_HEADER *out)
{
UINT32 path_count = 0, mode_count = 0;
if (GetDisplayConfigBufferSizes(
QDC_ONLY_ACTIVE_PATHS, &path_count, &mode_count) != ERROR_SUCCESS)
return false;
vector<DISPLAYCONFIG_PATH_INFO> paths(path_count);
vector<DISPLAYCONFIG_MODE_INFO> modes(mode_count);
if (QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &path_count, paths.data(),
&mode_count, modes.data(), nullptr) != ERROR_SUCCESS)
return false;
for (UINT32 i = 0; i < path_count; i++) {
DISPLAYCONFIG_SOURCE_DEVICE_NAME source{};
source.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
source.header.size = sizeof source;
source.header.adapterId = paths[i].sourceInfo.adapterId;
source.header.id = paths[i].sourceInfo.id;
if (DisplayConfigGetDeviceInfo(&source.header) != ERROR_SUCCESS ||
wcscmp(source.viewGdiDeviceName, name) != 0)
continue;
out->adapterId = paths[i].targetInfo.adapterId;
out->id = paths[i].targetInfo.id;
return true;
}
return false;
}
// The HDR form, the primaries and the peak of the output showing `monitor`.
static bool
dxgi_output_desc(HMONITOR monitor, DXGI_OUTPUT_DESC1 *out)
{
IDXGIFactory1 *factory = nullptr;
if (FAILED(CreateDXGIFactory1(IID_PPV_ARGS(&factory))))
return false;
bool found = false;
IDXGIAdapter1 *adapter = nullptr;
for (UINT i = 0; !found && SUCCEEDED(factory->EnumAdapters1(i, &adapter));
i++) {
IDXGIOutput *output = nullptr;
for (UINT j = 0; !found && SUCCEEDED(adapter->EnumOutputs(j, &output));
j++) {
DXGI_OUTPUT_DESC desc{};
IDXGIOutput6 *output6 = nullptr;
if (SUCCEEDED(output->GetDesc(&desc)) && desc.Monitor == monitor &&
SUCCEEDED(output->QueryInterface(IID_PPV_ARGS(&output6)))) {
found = SUCCEEDED(output6->GetDesc1(out));
output6->Release();
}
output->Release();
}
adapter->Release();
}
factory->Release();
return found;
}
// Advanced Color is on in its HDR form, and in its wide-gamut SDR form,
// which is Windows 11's automatic colour management on SDR displays.
// DisplayConfig tells that it is on, DXGI which form it takes.
static AdvancedColor
load_advanced_color(HMONITOR monitor, const wchar_t *name)
{
AdvancedColor result;
DISPLAYCONFIG_DEVICE_INFO_HEADER target{};
if (!display_config_target(name, &target))
return result;
// Set in both forms from Windows 11 22H2 on: observed, not documented.
DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO info{};
info.header = target;
info.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO;
info.header.size = sizeof info;
if (DisplayConfigGetDeviceInfo(&info.header) != ERROR_SUCCESS ||
!info.advancedColorEnabled)
return result;
DXGI_OUTPUT_DESC1 desc{};
if (!dxgi_output_desc(monitor, &desc))
return result;
result.active = true;
result.range.hdr =
desc.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020;
result.range.primaries =
array<double, 8>{desc.RedPrimary[0], desc.RedPrimary[1],
desc.GreenPrimary[0], desc.GreenPrimary[1], desc.BluePrimary[0],
desc.BluePrimary[1], desc.WhitePoint[0], desc.WhitePoint[1]};
// In units of 80/1000 cd/m², so that the UI matches the system's.
DISPLAYCONFIG_SDR_WHITE_LEVEL white{};
white.header = target;
white.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL;
white.header.size = sizeof white;
if (DisplayConfigGetDeviceInfo(&white.header) == ERROR_SUCCESS &&
white.SDRWhiteLevel)
result.white = float(white.SDRWhiteLevel) / 1000;
// The wide-gamut form has nothing above SDR white, and a high SDR white
// on a dim panel can take the HDR form to 1 or below.
if (result.range.hdr)
result.range.headroom = desc.MaxLuminance / (result.white * 80);
return result;
}
// The monitor showing `screen`, and its GDI device name in `monitor`.
static HMONITOR
screen_monitor(QScreen *screen, MONITORINFOEXW *monitor)
{
auto *native = screen
? screen->nativeInterface<QNativeInterface::QWindowsScreen>()
: nullptr;
monitor->cbSize = sizeof *monitor;
if (!native || !GetMonitorInfoW(native->handle(), monitor))
return nullptr;
return native->handle();
}
AdvancedColor
windows_advanced_color(QScreen *screen)
{
MONITORINFOEXW monitor{};
HMONITOR handle = screen_monitor(screen, &monitor);
return handle ? load_advanced_color(handle, monitor.szDevice)
: AdvancedColor{};
}
static DisplayProfile
load_display_profile(QScreen *screen)
{
DisplayProfile result;
if (!screen)
return result;
auto *native = screen->nativeInterface<QNativeInterface::QWindowsScreen>();
if (!native)
return result;
MONITORINFOEXW monitor{};
monitor.cbSize = sizeof monitor;
if (!GetMonitorInfoW(native->handle(), &monitor))
HMONITOR handle = screen_monitor(screen, &monitor);
if (!handle)
return result;
result.advanced_color = load_advanced_color(handle, monitor.szDevice);
HDC dc = CreateDCW(L"DISPLAY", monitor.szDevice, nullptr, nullptr);
if (!dc)
return result;
@@ -71,7 +199,7 @@ load_display_profile(QScreen *screen)
const QByteArray bytes = file.readAll();
result.icc.assign(bytes.begin(), bytes.end());
if (result.icc.empty())
return {};
return result;
result.source = "Windows ICM";
result.label = filename.toUtf8().toStdString();
qInfo("ICC source: Windows ICM (%s)", result.label.c_str());
@@ -137,10 +265,28 @@ Watch::arm()
namespace
{
// Advanced Color toggles change the profile without touching the registry.
struct DisplayChangeFilter final : QAbstractNativeEventFilter {
function<void()> on_change;
bool nativeEventFilter(
const QByteArray &type, void *message, qintptr *) override
{
if (type == "windows_generic_MSG" &&
static_cast<const MSG *>(message)->message == WM_DISPLAYCHANGE &&
this->on_change)
this->on_change();
return false;
}
};
struct WcsSource final : DisplayProfileSource {
function<void()> on_change;
Watch system;
Watch user;
unique_ptr<DisplayChangeFilter> display_change;
~WcsSource() override;
void start(function<void()> fn) override;
DisplayProfile load(QScreen *screen) override;
@@ -149,6 +295,13 @@ struct WcsSource final : DisplayProfileSource {
} // namespace
WcsSource::~WcsSource()
{
if (this->display_change && QCoreApplication::instance())
QCoreApplication::instance()->removeNativeEventFilter(
this->display_change.get());
}
bool
WcsSource::bind(Watch &watch, HKEY root, const wchar_t *path)
{
@@ -171,6 +324,12 @@ void
WcsSource::start(function<void()> fn)
{
this->on_change = std::move(fn);
if (!this->display_change) {
this->display_change = make_unique<DisplayChangeFilter>();
QCoreApplication::instance()->installNativeEventFilter(
this->display_change.get());
}
this->display_change->on_change = this->on_change;
if (this->system.notifier || this->user.notifier)
return;
+5 -2
View File
@@ -50,16 +50,19 @@ DisplayProfileWatch::unlisten(const void *key)
[key](const auto &item) { return item.first == key; });
}
// Windows sends WM_DISPLAYCHANGE to every top-level window, and registry
// changes come in bursts: whatever arrives before the listeners run is one.
void
DisplayProfileWatch::notify() const
DisplayProfileWatch::notify()
{
QObject *app = QCoreApplication::instance();
if (!app)
if (!app || exchange(this->notify_pending_, true))
return;
QMetaObject::invokeMethod(
app,
[this] {
this->notify_pending_ = false;
const auto copy = this->listeners_;
for (const auto &item : copy)
if (item.second)
+55 -1
View File
@@ -7,8 +7,10 @@
#pragma once
#include <array>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
@@ -18,10 +20,30 @@ class QScreen;
namespace dn
{
/// An output's dynamic range, as far as its platform tells.
struct DisplayRange {
bool hdr = false; ///< An HDR display, whatever its current headroom
float headroom = 1; ///< Current, linear, where 1 is SDR white
/// CIE 1931 xy of the output's primaries (R, G, B), then white.
std::optional<std::array<double, 8>> primaries;
};
/// Windows Advanced Color, as DXGI and DisplayConfig report it.
struct AdvancedColor {
/// In either its HDR or its wide-gamut SDR form.
bool active = false;
/// `hdr` is the HDR form; `primaries` come from DXGI.
DisplayRange range;
/// SDR white in scRGB, where 1.0 is 80 cd/m².
float white = 1;
};
struct DisplayProfile {
std::vector<unsigned char> icc;
std::string source;
std::string label;
/// Only Windows fills this in.
AdvancedColor advanced_color;
};
/// Platform lookup and change notification for display profiles.
@@ -35,6 +57,37 @@ public:
std::unique_ptr<DisplayProfileSource> make_display_profile_source();
#ifdef __APPLE__
/// The screen's EDR range: an HDR display when its potential headroom
/// exceeds 1, and its current headroom.
DisplayRange macos_display_range(QScreen *screen);
/// Calls `fn` on every change of screen parameters, EDR ramps included,
/// for as long as the returned handle lives.
std::shared_ptr<void> macos_watch_screen_parameters(std::function<void()> fn);
#else
inline DisplayRange
macos_display_range(QScreen *)
{
return {};
}
inline std::shared_ptr<void>
macos_watch_screen_parameters(std::function<void()>)
{
return {};
}
#endif
#ifdef _WIN32
/// Only the Advanced Color part of the screen's DisplayProfile, cheaper.
AdvancedColor windows_advanced_color(QScreen *screen);
#else
inline AdvancedColor
windows_advanced_color(QScreen *)
{
return {};
}
#endif
/// Process-wide display ICC lookup and change notification.
class DisplayProfileWatch
{
@@ -50,10 +103,11 @@ public:
void unlisten(const void *key);
private:
void notify() const;
void notify();
std::vector<std::pair<void *, std::function<void()>>> listeners_;
std::unique_ptr<DisplayProfileSource> source_;
bool notify_pending_ = false;
};
} // namespace dn
+1 -1
View File
@@ -554,7 +554,7 @@ dialog_settings(Kit &kit, SettingsDraft draft,
auto state = make_shared<SettingsDraft>(std::move(draft));
const char *const thumb_label = N_("Default _thumbnail size");
const char *const icc_label = N_("ICC _profile override");
const char *const icc_label = N_("ICC _profile override (when applicable)");
auto col = make_unique<GutterColumn>();
col->gap = 4.f;
+9 -1
View File
@@ -50,13 +50,21 @@ Cropper::load_working(const vector<uint8_t> &data)
if (!image)
return false;
// libjpeg-turbo transforms keep nothing past the primary image's EOI,
// such as gain maps or depth maps.
if (grid.mpf_images)
this->message_ = dawn::format_message(
P_("Saving drops %u embedded image",
"Saving drops %u embedded images", grid.mpf_images),
grid.mpf_images);
this->image_ = std::move(image);
this->grid_ = grid;
this->exif_ = dawn::orientation_or_0(this->image_->orientation);
if (this->kit_.renderer_) {
const auto &im = *this->image_;
this->kit_.renderer_->set_image(
im.width, im.height, im.data.data(), im.stride);
im.width, im.height, im.data.data(), im.stride, nullptr);
}
return true;
}
+44 -4
View File
@@ -116,6 +116,21 @@ constexpr ToolbarSpec kItems[] = {
{Slot::Right, Action::Fullscreen},
};
// The HDR rendition needs a window that can present it, a display that
// can show it, and a page that has it.
static bool
hdr_available(const Viewer &v)
{
return v.screen_capable_ && v.screen_hdr_ && v.current_ &&
v.current_->gain_map;
}
static bool
hdr_rendition(const Viewer &v)
{
return v.hdr_ && hdr_available(v);
}
static bool
spec_enabled(const Viewer &v, Action action)
{
@@ -144,6 +159,10 @@ spec_enabled(const Viewer &v, Action action)
return QFileInfo(url_to_path(v.url_)).isFile();
case Action::Smooth:
return !(v.current_ && v.current_->render);
case Action::HighDynamicRange:
return hdr_available(v);
case Action::NonlinearProcessing:
return !hdr_rendition(v);
case Action::SaveAs:
return bool(v.current_);
case Action::SaveFrameAs:
@@ -162,7 +181,9 @@ spec_active(const Viewer &v, Action action)
case Action::Checkerboard:
return v.checkerboard_;
case Action::NonlinearProcessing:
return v.nonlinear_processing_;
return v.nonlinear_processing_ && !hdr_rendition(v);
case Action::HighDynamicRange:
return v.hdr_;
case Action::BrowserDelays:
return v.browser_delays_;
case Action::ColorManagement:
@@ -397,8 +418,10 @@ upload_frame(const Viewer &v, const dawn::Image &image)
{
if (!v.kit_.renderer_)
return;
v.kit_.renderer_->set_image(
image.width, image.height, image.data.data(), image.stride);
// Maps are per page, and only still images have them.
v.kit_.renderer_->set_image(image.width, image.height, image.data.data(),
image.stride,
&image == v.current_.get() ? v.current_->gain_map.get() : nullptr);
}
constexpr int64_t kBrowserDelayFloorMs = 11;
@@ -889,6 +912,9 @@ decode_open(const OpenJob &open, const shared_ptr<dawn::Cmm> &cmm)
ctx.screen_profile->to_bytes());
ctx.screen_dpi = open.dpi;
ctx.enhance = open.key.enhance;
// Whatever the display, so that headroom and the HDR toggle never
// need a reload.
ctx.gain_maps = true;
if (open.loaders)
ctx.loaders = *open.loaders;
@@ -1797,6 +1823,10 @@ apply_action(Viewer &v, Action action)
v.nonlinear_processing_ = !v.nonlinear_processing_;
request_render(v);
return true;
case Action::HighDynamicRange:
v.hdr_ = !v.hdr_;
request_render(v);
return true;
case Action::BrowserDelays:
v.browser_delays_ = !v.browser_delays_;
request_render(v);
@@ -1909,6 +1939,9 @@ apply_view(const Viewer &v)
snap_pan_to_pixels(&pan_y, float(dh), float(vp.height), gpu_scale);
}
}
// Nothing above SDR white has an encoded-value compositing convention,
// so the HDR rendition is processed linearly, even at zero weight.
const bool hdr = hdr_rendition(v);
renderer.view = {
.scale = gpu_scale,
.pan_x = pan_x,
@@ -1916,9 +1949,13 @@ apply_view(const Viewer &v)
.angle = v.angle_,
.orientation = v.orientation_,
.checkerboard = v.checkerboard_,
.nonlinear_processing = v.nonlinear_processing_,
.nonlinear_processing = v.nonlinear_processing_ && !hdr,
.checker_size = float(v.kit_.px(kCheckPts)),
.filter = v.filter_ ? renderer.preferred_filter : dawn::Filter::Nearest,
.hdr = hdr,
.gain_weight = hdr
? dawn::gain_map_weight(*v.current_->gain_map, v.screen_headroom_)
: 0.f,
};
}
@@ -2112,6 +2149,9 @@ Viewer::screen_changed(
this->screen_profile_ = state.profile;
this->screen_colour_ = state.colour;
this->screen_profile_fallback_ = state.fallback;
this->screen_capable_ = state.capable;
this->screen_hdr_ = state.hdr;
this->screen_headroom_ = state.headroom;
if (reload) {
this->restore_view_ = {true, this->scale_, this->pan_x_, this->pan_y_,
this->orientation_, this->angle_, this->view_locked_};
+4
View File
@@ -114,6 +114,10 @@ struct Viewer : Widget {
bool filter_ = true;
bool checkerboard_ = true;
bool nonlinear_processing_ = false;
bool hdr_ = true;
bool screen_capable_ = false;
bool screen_hdr_ = false;
float screen_headroom_ = 1;
bool browser_delays_ = false;
dawn::Orientation orientation_ = dawn::Orientation::Rotate0;
enum class Drag : uint8_t { None, Pan, Zoom, Rotate };
+6
View File
@@ -49,6 +49,12 @@ struct ScreenState {
std::shared_ptr<dawn::Profile> profile;
std::shared_ptr<const ScreenColour> colour;
bool fallback = true;
/// The window can present extended range at all.
bool capable = false;
/// The output is an HDR display, whatever its current headroom.
bool hdr = false;
/// Linear, where 1 is SDR white.
float headroom = 1;
};
struct [[nodiscard]] Size {
+141 -24
View File
@@ -53,6 +53,8 @@ vk_format_name(VkFormat f)
switch (f) {
case VK_FORMAT_R16G16B16A16_UNORM:
return "R16G16B16A16_UNORM";
case VK_FORMAT_R16G16B16A16_SFLOAT:
return "R16G16B16A16_SFLOAT";
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
return "A2B10G10R10_UNORM_PACK32";
case VK_FORMAT_A2R10G10B10_UNORM_PACK32:
@@ -74,6 +76,8 @@ vk_colorspace_name(VkColorSpaceKHR cs)
return "PASS_THROUGH";
case VK_COLOR_SPACE_SRGB_NONLINEAR_KHR:
return "SRGB_NONLINEAR";
case VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT:
return "EXTENDED_SRGB_LINEAR";
default:
return "other";
}
@@ -103,9 +107,22 @@ colorspace_score(VkColorSpaceKHR cs)
return 0;
}
// Extended presentation is linear, with a float swapchain in this space.
#if defined _WIN32 || defined __APPLE__
constexpr VkColorSpaceKHR kExtendedColorSpace =
VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT;
#else
constexpr VkColorSpaceKHR kExtendedColorSpace = VK_COLOR_SPACE_PASS_THROUGH_EXT;
#endif
constexpr VkFormat kExtendedFormat = VK_FORMAT_R16G16B16A16_SFLOAT;
static int
surface_format_score(const VkSurfaceFormatKHR &sf)
{
// The encoded present pass is for display RGB, which a float surface
// is not.
if (sf.format == kExtendedFormat)
return -1;
return colorspace_score(sf.colorSpace) * 100 + format_bits(sf.format);
}
@@ -122,9 +139,14 @@ dither_bits(VkFormat format)
return format_bits(format);
}
static VkSurfaceFormatKHR
pick_surface_format(const vector<VkSurfaceFormatKHR> &formats)
VkSurfaceFormatKHR
pick_surface_format(const vector<VkSurfaceFormatKHR> &formats, bool extended)
{
for (const VkSurfaceFormatKHR &sf : formats)
if (extended && sf.format == kExtendedFormat &&
sf.colorSpace == kExtendedColorSpace)
return sf;
VkSurfaceFormatKHR best = formats.front();
int best_score = surface_format_score(best);
for (size_t i = 1; i < formats.size(); i++) {
@@ -164,6 +186,16 @@ static constexpr VkDeviceSize kThumbAtlasHeapFrac = 4;
namespace
{
struct PresentPush {
float levels;
uint32_t premultiplied;
uint32_t srgb_attachment;
uint32_t extended;
float matrix[3][4];
float white;
};
static_assert(sizeof(PresentPush) == 68);
struct PushConstant {
float scale[2];
float translate[2];
@@ -312,11 +344,20 @@ Renderer::destroy()
this->present_queued_ = {};
}
// DN_BPC would dither even a float surface, so this is explicit.
bool
Renderer::dithering() const
{
const int bits = dither_bits(this->format_);
return this->dither_enabled_ && bits > 0 && bits <= 10;
return !this->extended_ && this->dither_enabled_ && bits > 0 && bits <= 10;
}
// Encoded presentation keeps its UNORM composition, bit for bit.
VkFormat
Renderer::compose_format() const
{
return this->extended_ ? VK_FORMAT_R16G16B16A16_SFLOAT
: VK_FORMAT_R16G16B16A16_UNORM;
}
void
@@ -327,7 +368,7 @@ Renderer::ensure_engine()
string error;
if (!this->engine_.init(this->phys_, this->device_, this->queue_,
this->queue_family_, VK_FORMAT_R16G16B16A16_UNORM,
this->queue_family_, compose_format(),
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, &error))
die(error.c_str());
if (!this->encoding_)
@@ -347,6 +388,27 @@ Renderer::set_encoding(shared_ptr<const dawn::ProfileEncoding> encoding)
die(error.c_str());
}
void
Renderer::set_presentation(PresentationTarget target)
{
this->presentation_ = std::move(target);
}
bool
Renderer::offers_extended() const
{
if (!this->phys_ || !this->surface_)
return false;
uint32_t count = 0;
vkGetPhysicalDeviceSurfaceFormatsKHR(
this->phys_, this->surface_, &count, nullptr);
vector<VkSurfaceFormatKHR> formats(count);
vkGetPhysicalDeviceSurfaceFormatsKHR(
this->phys_, this->surface_, &count, formats.data());
return !formats.empty() &&
pick_surface_format(formats, true).format == kExtendedFormat;
}
void
Renderer::create_swapchain()
{
@@ -377,7 +439,9 @@ Renderer::create_swapchain()
const VkFormat old_format = this->format_;
const VkColorSpaceKHR old_color_space = this->color_space_;
const VkSurfaceFormatKHR picked = pick_surface_format(formats);
const VkSurfaceFormatKHR picked =
pick_surface_format(formats, this->extended_);
this->extended_ = picked.format == kExtendedFormat;
this->format_ = picked.format;
this->color_space_ = picked.colorSpace;
if (this->format_ != old_format)
@@ -387,10 +451,15 @@ Renderer::create_swapchain()
vk_format_name(this->format_),
vk_colorspace_name(this->color_space_),
dithering() ? dither_bits(this->format_) : 0);
if (this->color_space_ != VK_COLOR_SPACE_PASS_THROUGH_EXT)
if (this->color_space_ != VK_COLOR_SPACE_PASS_THROUGH_EXT &&
!this->extended_)
qWarning("swapchain: PASS_THROUGH unavailable; "
"using compositor-managed sRGB");
}
if (this->engine_.dest_format() != compose_format()) {
ensure_engine();
this->overlay_.set_render_pass(this->engine_.dest_render_pass());
}
if (this->extent_.width == 0 || this->extent_.height == 0)
return;
@@ -497,14 +566,15 @@ Renderer::wait_idle() const
}
void
Renderer::set_image(
uint32_t width, uint32_t height, const uint8_t *pixels, size_t stride)
Renderer::set_image(uint32_t width, uint32_t height, const uint8_t *pixels,
size_t stride, const dawn::GainMap *map)
{
if (!this->device_ || width == 0 || height == 0 || !pixels)
return;
wait_idle();
string error;
if (!this->engine_.set_image(width, height, pixels, stride, &error))
if (!this->engine_.set_image(width, height, pixels, stride, &error) ||
(map && !this->engine_.set_gain_map(map, &error)))
die(error.c_str());
}
@@ -582,6 +652,35 @@ Renderer::draw_frame(const OverlayMesh &mesh, bool show_image)
return true;
CALL_VK(WaitForFences, "", this->device_, 1, &this->fence_, VK_TRUE,
UINT64_MAX);
// The frame's presentation is fixed before recording, as its pipelines
// and white are recorded, and a change of it takes a new swapchain.
// A frame the platform does not take drops what it latched.
const bool draw_image = show_image && this->engine_.has_image();
const bool hdr_page = draw_image && this->view.hdr;
const PresentationTarget &target = this->presentation_;
auto settle = [&](Presentation wanted, float *white) {
*white = target.white;
return target.latch ? target.latch(wanted, white) : wanted;
};
Presentation wanted = Presentation::Encoded;
if (target.capable && (target.always || hdr_page))
wanted = hdr_page ? Presentation::Hdr : Presentation::Sdr;
float white = 1;
Presentation presentation = settle(wanted, &white);
if ((presentation != Presentation::Encoded) != this->extended_) {
this->extended_ = !this->extended_;
create_swapchain();
// The format list may change with HDR before the profile does:
// wait for the next set_presentation() rather than retry every frame.
if (!this->extended_ && presentation != Presentation::Encoded) {
this->presentation_.capable = false;
presentation = settle(Presentation::Encoded, &white);
}
if (!this->swapchain_)
return true;
}
uint32_t index = 0;
// A hidden Wayland surface has no guaranteed presentation progress, so an
// infinite acquire timeout is invalid. Keep the latest frame dirty and let
@@ -609,6 +708,10 @@ Renderer::draw_frame(const OverlayMesh &mesh, bool show_image)
const auto checker = dawn::sample_curves(this->encoding_->encode,
{this->checker_[0], this->checker_[1], this->checker_[2]});
dawn::ScaleView view = this->view;
// Gain would clip in an encoded composition, and the SDR variant of
// a Wayland surface declares nothing above SDR white.
if (presentation != Presentation::Hdr)
view.gain_weight = 0;
// dn composes in linear light; image alpha resolves against the well.
view.profile_curves = true;
view.output_encoding = dawn::ScaleEncoding::Linear;
@@ -619,7 +722,6 @@ Renderer::draw_frame(const OverlayMesh &mesh, bool show_image)
const auto well = dawn::sample_curves(this->encoding_->encode,
{this->well_[0], this->well_[1], this->well_[2]});
const float clear[4] = {well[0], well[1], well[2], this->well_[3]};
const bool draw_image = show_image && this->engine_.has_image();
string error;
if (draw_image &&
!this->engine_.prepare(this->cmd_, this->extent_.width,
@@ -631,7 +733,7 @@ Renderer::draw_frame(const OverlayMesh &mesh, bool show_image)
this->extent_.height, view, clear, area);
this->overlay_.record(this->cmd_, mesh, this->extent_);
vkCmdEndRenderPass(this->cmd_);
record_presentation(this->cmd_, this->framebuffers_[index]);
record_presentation(this->cmd_, this->framebuffers_[index], white);
CALL_VK(EndCommandBuffer, "", this->cmd_);
VkPipelineStageFlags wait_stage =
@@ -689,11 +791,11 @@ Renderer::destroy_compose()
void
Renderer::create_compose()
{
constexpr VkFormat kCompose = VK_FORMAT_R16G16B16A16_UNORM;
const VkFormat compose = compose_format();
VkImageCreateInfo image_info{
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
.imageType = VK_IMAGE_TYPE_2D,
.format = kCompose,
.format = compose,
.extent = {this->extent_.width, this->extent_.height, 1},
.mipLevels = 1,
.arrayLayers = 1,
@@ -726,7 +828,7 @@ Renderer::create_compose()
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
.image = this->compose_image_,
.viewType = VK_IMAGE_VIEW_TYPE_2D,
.format = kCompose,
.format = compose,
.subresourceRange = {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.levelCount = 1,
.layerCount = 1},
@@ -891,7 +993,7 @@ Renderer::create_presentation()
VkPushConstantRange push{
.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
.offset = 0,
.size = sizeof(float) + 2 * sizeof(uint32_t),
.size = sizeof(PresentPush),
};
VkPipelineLayoutCreateInfo layout_info{
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
@@ -1013,7 +1115,8 @@ begin_render_pass(VkCommandBuffer cmd, VkRenderPass pass, VkFramebuffer dest,
}
void
Renderer::record_presentation(VkCommandBuffer cmd, VkFramebuffer dest) const
Renderer::record_presentation(
VkCommandBuffer cmd, VkFramebuffer dest, float white) const
{
if (!cmd || !dest || !this->compose_image_ || !this->presentation_pipe_)
return;
@@ -1040,16 +1143,20 @@ Renderer::record_presentation(VkCommandBuffer cmd, VkFramebuffer dest) const
cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, this->presentation_pipe_);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
this->presentation_layout_, 0, 1, &this->presentation_set_, 0, nullptr);
const struct {
float levels;
uint32_t premultiplied;
uint32_t srgb_attachment;
} push{
dithering() ? float((1 << dither_bits(this->format_)) - 1) : 0.f,
this->composite_alpha_ != VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR,
this->format_ == VK_FORMAT_B8G8R8A8_SRGB ||
PresentPush push{
.levels =
dithering() ? float((1 << dither_bits(this->format_)) - 1) : 0.f,
.premultiplied = this->composite_alpha_ !=
VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR,
.srgb_attachment = this->format_ == VK_FORMAT_B8G8R8A8_SRGB ||
this->format_ == VK_FORMAT_R8G8B8A8_SRGB,
.extended = this->extended_,
.matrix = {},
.white = white,
};
for (size_t c = 0; c < 3; c++)
for (size_t r = 0; r < 3; r++)
push.matrix[c][r] = float(this->presentation_.matrix[c][r]);
vkCmdPushConstants(cmd, this->presentation_layout_,
VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof push, &push);
vkCmdDraw(cmd, 3, 1, 0, 0);
@@ -1139,6 +1246,16 @@ OverlayVulkan::init(VkPhysicalDevice phys, VkDevice device, VkQueue queue,
return create_pipeline(render_pass);
}
void
OverlayVulkan::set_render_pass(VkRenderPass render_pass)
{
if (!this->device_)
return;
vkDeviceWaitIdle(this->device_);
destroy_pipeline();
create_pipeline(render_pass);
}
bool
OverlayVulkan::create_pipeline(VkRenderPass render_pass)
{
+40 -3
View File
@@ -26,6 +26,30 @@
namespace dn
{
/// How a frame is presented: encoded for the display profile, or extended
/// range in the platform's linear space, without or with luminance above
/// SDR white.
enum class Presentation : uint8_t { Encoded, Sdr, Hdr };
/// What the window finds out about extended presentation.
struct PresentationTarget {
/// Whether the window can present extended at all.
bool capable = false;
/// Whether capability alone decides, rather than a drawn HDR page.
bool always = false;
/// From display linear RGB into the platform's, and its SDR white.
dawn::RgbMatrix matrix = {{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}};
float white = 1;
/// Settles what a frame gets, as the platform may only have a lesser
/// presentation ready, and replaces `white` for it. Null grants all.
std::function<Presentation(Presentation wanted, float *white)> latch;
};
/// The swapchain format for either presentation. Encoded never takes a
/// float format; extended falls back to encoded without its own.
VkSurfaceFormatKHR pick_surface_format(
const std::vector<VkSurfaceFormatKHR> &formats, bool extended);
struct AtlasUpload {
const uint16_t *pixels = nullptr;
int width = 0;
@@ -113,6 +137,8 @@ public:
bool init(VkPhysicalDevice phys, VkDevice device, VkQueue queue,
uint32_t queue_family, VkRenderPass render_pass);
/// Rebuilds the pipelines for another composition pass, keeping atlases.
void set_render_pass(VkRenderPass render_pass);
void set_encoding_buffer(VkDescriptorBufferInfo info);
// Upload requests copy borrowed pixels immediately; GPU work is deferred.
bool upload_font(
@@ -143,8 +169,10 @@ class Renderer
void create_presentation_pipeline();
void destroy_presentation();
void create_presentation();
void record_presentation(VkCommandBuffer cmd, VkFramebuffer dest) const;
void record_presentation(
VkCommandBuffer cmd, VkFramebuffer dest, float white) const;
[[nodiscard]] bool dithering() const;
[[nodiscard]] VkFormat compose_format() const;
VkSurfaceKHR surface_ = VK_NULL_HANDLE; // borrowed from QWindow
VkPhysicalDevice phys_ = VK_NULL_HANDLE; // borrowed from GpuContext
@@ -188,6 +216,8 @@ class Renderer
VkDescriptorPool presentation_pool_ = VK_NULL_HANDLE;
VkDescriptorSet presentation_set_ = VK_NULL_HANDLE;
bool needs_resize_ = false;
bool extended_ = false;
PresentationTarget presentation_;
bool prefer_premultiplied_ = false;
bool dither_enabled_ = true;
uint32_t dest_inset_ = 0;
@@ -210,8 +240,8 @@ public:
VkPresentModeKHR preferred_present_mode,
std::function<void()> present_about_to_queue,
std::function<void()> present_queued);
void set_image(
uint32_t w, uint32_t h, const uint8_t *pixels, size_t stride);
void set_image(uint32_t w, uint32_t h, const uint8_t *pixels,
size_t stride, const dawn::GainMap *map);
void clear_image();
void set_well_colour(float r, float g, float b);
void set_prefer_premultiplied(bool enabled)
@@ -222,6 +252,13 @@ public:
void set_dest_inset(uint32_t px) { this->dest_inset_ = px; }
void set_checker_colour(float r, float g, float b);
void set_encoding(std::shared_ptr<const dawn::ProfileEncoding> encoding);
void set_presentation(PresentationTarget target);
/// SDR white for extended frames, where no latch gives it: Windows's
/// follows a slider that no notification reports.
void set_white(float white) { this->presentation_.white = white; }
/// Whether the surface has the format extended presentation needs.
/// Asked anew each time, as drivers may change it with the display mode.
[[nodiscard]] bool offers_extended() const;
bool upload_font(
const uint16_t *pixels, int width, int height, Sheet::Packed dirty);
[[nodiscard]] int thumb_atlas_max() const;
+464 -26
View File
@@ -6,17 +6,23 @@
//
#include "wayland-color-bridge.hpp"
#include "window.hpp"
#include <libdn/ipc-shm.hpp>
#include <QByteArray>
#include <QCoreApplication>
#include <QEvent>
#include <QGuiApplication>
#include <QWindow>
#include <QMetaObject>
#include <QtGui/qguiapplication_platform.h>
#include <QtLogging>
#include <qpa/qplatformnativeinterface.h>
#include <cstring>
#include <utility>
#include <unistd.h>
using namespace std;
@@ -72,31 +78,201 @@ WaylandColorBridge::request_preferred()
{
if (!this->feedback_)
return;
if (this->pending_description_)
wp_image_description_v1_destroy(this->pending_description_);
this->pending_description_ =
if (this->pending_info_)
wp_image_description_info_v1_destroy(this->pending_info_);
this->pending_info_ = nullptr;
if (this->preferred_)
wp_image_description_v1_destroy(this->preferred_);
this->preferred_ =
wp_color_management_surface_feedback_v1_get_preferred(this->feedback_);
wp_image_description_v1_add_listener(
this->pending_description_, &kDescriptionListener, this);
this->preferred_, &kDescriptionListener, this);
wl_display_flush(this->display_);
}
// Tagging with the preferred description passes our pixels through untouched,
// which is right while that is an SDR encoding: we have already converted
// to the display profile, and the compositor's idea of the output may well be
// wrong (Mutter's sdr-native assumes EDID primaries). An HDR output cannot be
// matched by SDR pixels, so there we describe what they really are.
void
WaylandColorBridge::apply_description(wp_image_description_v1 *description)
WaylandColorBridge::describe_icc()
{
if (description != this->pending_description_)
return;
if (this->icc_pending_)
wp_image_description_v1_destroy(this->icc_pending_);
this->icc_pending_ = nullptr;
wp_color_management_surface_v1_set_image_description(this->color_surface_,
description, WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL);
wp_image_description_v1_destroy(description);
this->pending_description_ = nullptr;
qInfo("Wayland CM identity: preferred description applied");
// Other sizes are a fatal protocol error, and "32 MB" is read both ways.
// libwayland duplicates the descriptor while marshalling the request.
dawn::ipc::SharedMemory file;
if (!this->icc_.empty() && this->icc_.size() <= 32'000'000)
file =
dawn::ipc::SharedMemory::copy(this->icc_.data(), this->icc_.size());
if (file.handle() < 0) {
qWarning("Wayland CM identity unavailable: HDR output, and no "
"display profile to describe our pixels with");
this->encoded_icc_ = false;
drop(this->encoded_);
set_unmatched(true);
return;
}
auto *creator = wp_color_manager_v1_create_icc_creator(this->manager_);
wp_image_description_creator_icc_v1_set_icc_file(
creator, int32_t(file.handle()), 0, uint32_t(this->icc_.size()));
this->icc_pending_ = wp_image_description_creator_icc_v1_create(creator);
wp_image_description_v1_add_listener(
this->icc_pending_, &kDescriptionListener, this);
wl_display_flush(this->display_);
}
// Our pixels in extended linear light, within the display's own primaries,
// with SDR white at the preferred reference, and nothing above `peak`.
wp_image_description_v1 *
WaylandColorBridge::describe_parametric(uint32_t peak)
{
auto *creator =
wp_color_manager_v1_create_parametric_creator(this->manager_);
wp_image_description_creator_params_v1_set_tf_named(
creator, WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR);
const auto &p = this->primaries_;
if (this->set_primaries_)
wp_image_description_creator_params_v1_set_primaries(
creator, p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7]);
else
wp_image_description_creator_params_v1_set_primaries_named(
creator, WP_COLOR_MANAGER_V1_PRIMARIES_BT2020);
// Mutter rejects a reference above the maximum.
wp_image_description_creator_params_v1_set_luminances(creator,
this->info_.target_min, peak, min(this->info_.reference, peak));
auto *description = wp_image_description_creator_params_v1_create(creator);
wp_image_description_v1_add_listener(
description, &kDescriptionListener, this);
wl_display_flush(this->display_);
return description;
}
// At most one in flight, so that a brightness drag does not queue up
// a description per step: the latest values wait for it.
void
WaylandColorBridge::describe_hdr()
{
if (this->hdr_pending_) {
this->hdr_stale_ = true;
return;
}
this->hdr_stale_ = false;
const uint32_t peak = this->info_.target_max;
this->hdr_pending_white_ =
float(min(this->info_.reference, peak)) / float(peak);
this->hdr_pending_ = describe_parametric(peak);
}
void
WaylandColorBridge::sync_variants(bool replace)
{
const bool wanted = this->describing_ && this->output_.extended &&
this->output_.range.hdr && this->info_.target_max > 0;
if (!wanted || replace) {
drop(this->sdr_);
drop(this->hdr_);
if (this->sdr_pending_)
wp_image_description_v1_destroy(this->sdr_pending_);
this->sdr_pending_ = nullptr;
if (this->hdr_pending_)
wp_image_description_v1_destroy(this->hdr_pending_);
this->hdr_pending_ = nullptr;
}
if (!wanted)
return;
if (!this->sdr_.object && !this->sdr_pending_)
this->sdr_pending_ = describe_parametric(
max(min(this->info_.reference, this->info_.target_max), 1u));
if (!this->hdr_.object && !this->hdr_pending_)
describe_hdr();
}
// A description the surface has may be destroyed: it keeps its own.
void
WaylandColorBridge::drop(Described &described)
{
if (described.object)
wp_image_description_v1_destroy(described.object);
described = {};
}
// The window renders in sRGB for the frames that go out without our profile.
void
WaylandColorBridge::set_unmatched(bool unmatched)
{
if (exchange(this->unmatched_, unmatched) != unmatched)
post_refresh(true);
}
// Not from within a listener, which the profile reload might reenter.
void
WaylandColorBridge::post_refresh(bool heavy)
{
if (!this->window_)
return;
QMetaObject::invokeMethod(
this->window_,
[window = this->window_, heavy] {
if (heavy)
window->handle_screen_change(window->screen());
else
window->refresh_headroom();
},
Qt::QueuedConnection);
}
void
WaylandColorBridge::ready(
Described &slot, wp_image_description_v1 *object, float white)
{
drop(slot);
slot = {object, this->next_id_++, white};
if (this->window_)
QCoreApplication::postEvent(
this->window_, new QEvent(QEvent::UpdateRequest));
}
Presentation
WaylandColorBridge::latch(Presentation wanted, float *white)
{
if (wanted == Presentation::Hdr && this->hdr_.object) {
this->latched_ = this->hdr_;
*white = this->hdr_.white;
return Presentation::Hdr;
}
if (wanted != Presentation::Encoded && this->sdr_.object) {
this->latched_ = this->sdr_;
*white = this->sdr_.white;
return Presentation::Sdr;
}
this->latched_ = this->encoded_;
return Presentation::Encoded;
}
// Mesa commits inside vkQueuePresentKHR, on this call stack, with no Qt
// handler in between, so the description goes out with the frame it is for.
void
WaylandColorBridge::apply_latched()
{
if (!this->color_surface_ || this->latched_.id == this->applied_)
return;
this->applied_ = this->latched_.id;
if (this->latched_.object)
wp_color_management_surface_v1_set_image_description(
this->color_surface_, this->latched_.object,
WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL);
else
wp_color_management_surface_v1_unset_image_description(
this->color_surface_);
}
void
WaylandColorBridge::registry_global(void *data, wl_registry *registry,
uint32_t name, const char *interface, uint32_t)
@@ -129,8 +305,52 @@ WaylandColorBridge::ignore_u32(void *, wp_color_manager_v1 *, uint32_t)
}
void
WaylandColorBridge::ignore_done(void *, wp_color_manager_v1 *)
WaylandColorBridge::supported_feature(
void *data, wp_color_manager_v1 *, uint32_t feature)
{
auto *self = static_cast<WaylandColorBridge *>(data);
switch (feature) {
case WP_COLOR_MANAGER_V1_FEATURE_ICC_V2_V4:
self->icc_supported_ = true;
break;
case WP_COLOR_MANAGER_V1_FEATURE_PARAMETRIC:
self->parametric_ = true;
break;
case WP_COLOR_MANAGER_V1_FEATURE_SET_PRIMARIES:
self->set_primaries_ = true;
break;
case WP_COLOR_MANAGER_V1_FEATURE_SET_LUMINANCES:
self->set_luminances_ = true;
break;
}
}
void
WaylandColorBridge::supported_tf_named(
void *data, wp_color_manager_v1 *, uint32_t tf)
{
if (tf == WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR)
static_cast<WaylandColorBridge *>(data)->ext_linear_ = true;
}
void
WaylandColorBridge::supported_primaries_named(
void *data, wp_color_manager_v1 *, uint32_t primaries)
{
if (primaries == WP_COLOR_MANAGER_V1_PRIMARIES_BT2020)
static_cast<WaylandColorBridge *>(data)->bt2020_ = true;
}
// The features are all in, and they decide what the output can take.
void
WaylandColorBridge::manager_done(void *data, wp_color_manager_v1 *)
{
auto *self = static_cast<WaylandColorBridge *>(data);
WaylandOutput &output = self->output_;
output.icc = self->icc_supported_;
output.own_primaries = self->set_primaries_;
output.extended = self->parametric_ && self->set_luminances_ &&
self->ext_linear_ && (self->set_primaries_ || self->bt2020_);
}
void
@@ -145,10 +365,26 @@ WaylandColorBridge::description_failed(void *data,
wp_image_description_v1 *description, uint32_t, const char *message)
{
auto *self = static_cast<WaylandColorBridge *>(data);
qWarning("Wayland CM identity: preferred description failed: %s",
const char *what = "preferred description";
if (description == self->icc_pending_) {
what = "display profile";
self->icc_pending_ = nullptr;
self->encoded_icc_ = false;
self->drop(self->encoded_);
self->set_unmatched(true);
} else if (description == self->sdr_pending_) {
what = "SDR description";
self->sdr_pending_ = nullptr;
} else if (description == self->hdr_pending_) {
what = "HDR description";
self->hdr_pending_ = nullptr;
if (self->hdr_stale_)
self->describe_hdr();
} else {
self->preferred_ = nullptr;
}
qWarning("Wayland CM identity: %s failed: %s", what,
message ? message : "unknown failure");
if (self->pending_description_ == description)
self->pending_description_ = nullptr;
wp_image_description_v1_destroy(description);
}
@@ -156,7 +392,145 @@ void
WaylandColorBridge::description_ready(
void *data, wp_image_description_v1 *description, uint32_t)
{
static_cast<WaylandColorBridge *>(data)->apply_description(description);
auto *self = static_cast<WaylandColorBridge *>(data);
if (description == self->icc_pending_) {
self->icc_pending_ = nullptr;
self->encoded_icc_ = true;
self->ready(self->encoded_, description, 1);
self->set_unmatched(false);
qInfo("Wayland CM identity: display profile ready");
} else if (description == self->sdr_pending_) {
self->sdr_pending_ = nullptr;
self->ready(self->sdr_, description, 1);
} else if (description == self->hdr_pending_) {
self->hdr_pending_ = nullptr;
self->ready(self->hdr_, description, self->hdr_pending_white_);
if (self->hdr_stale_)
self->describe_hdr();
} else {
self->pending_ = {};
self->pending_info_ =
wp_image_description_v1_get_information(description);
wp_image_description_info_v1_add_listener(
self->pending_info_, &kInfoListener, self);
wl_display_flush(self->display_);
}
}
void
WaylandColorBridge::info_icc_file(
void *, wp_image_description_info_v1 *, int32_t fd, uint32_t)
{
close(fd);
}
void
WaylandColorBridge::info_ignore_primaries(void *,
wp_image_description_info_v1 *, int32_t, int32_t, int32_t, int32_t, int32_t,
int32_t, int32_t, int32_t)
{
}
void
WaylandColorBridge::info_target_primaries(void *data,
wp_image_description_info_v1 *, int32_t r_x, int32_t r_y, int32_t g_x,
int32_t g_y, int32_t b_x, int32_t b_y, int32_t w_x, int32_t w_y)
{
auto *self = static_cast<WaylandColorBridge *>(data);
self->pending_.target_primaries = {r_x, r_y, g_x, g_y, b_x, b_y, w_x, w_y};
self->pending_.have_target_primaries = true;
}
void
WaylandColorBridge::info_ignore_u32(
void *, wp_image_description_info_v1 *, uint32_t)
{
}
void
WaylandColorBridge::info_luminances(void *data, wp_image_description_info_v1 *,
uint32_t min_lum, uint32_t max_lum, uint32_t reference_lum)
{
auto *self = static_cast<WaylandColorBridge *>(data);
self->pending_.min = min_lum;
self->pending_.max = max_lum;
self->pending_.reference = reference_lum;
self->pending_.parametric = true;
}
void
WaylandColorBridge::info_target_luminance(void *data,
wp_image_description_info_v1 *, uint32_t min_lum, uint32_t max_lum)
{
auto *self = static_cast<WaylandColorBridge *>(data);
self->pending_.target_min = min_lum;
self->pending_.target_max = max_lum;
}
// Everything the information brought takes effect together. Only the HDR
// display test and the target primaries need the screen profile refreshed;
// luminances just move the headroom, and the HDR variant with it.
void
WaylandColorBridge::info_done(void *data, wp_image_description_info_v1 *info)
{
auto *self = static_cast<WaylandColorBridge *>(data);
wp_image_description_info_v1_destroy(info);
self->pending_info_ = nullptr;
const Info old = self->info_;
const Info &now = self->info_ = self->pending_;
DisplayRange &range = self->output_.range;
const bool was_hdr = range.hdr;
range.hdr = now.parametric &&
(now.max > now.reference || now.target_max > now.reference);
range.headroom = 1;
if (now.parametric && now.reference)
range.headroom =
float(min(now.max, now.target_max ? now.target_max : now.max)) /
float(now.reference);
range.primaries.reset();
if (now.have_target_primaries) {
array<double, 8> xy{};
for (size_t i = 0; i < xy.size(); i++)
xy[i] = now.target_primaries[i] / 1e6;
range.primaries = xy;
}
if (!range.hdr) {
// An SDR output takes the preferred description as it is.
self->encoded_icc_ = false;
self->ready(self->encoded_, self->preferred_, 1);
self->preferred_ = nullptr;
self->set_unmatched(false);
} else {
wp_image_description_v1_destroy(self->preferred_);
self->preferred_ = nullptr;
if (!self->icc_supported_) {
// Nothing to tag with, and nothing that would call for sRGB.
self->encoded_icc_ = false;
self->drop(self->encoded_);
self->set_unmatched(false);
} else if (!self->encoded_icc_ && !self->icc_pending_) {
self->describe_icc();
}
}
const bool heavy = range.hdr != was_hdr ||
now.have_target_primaries != old.have_target_primaries ||
now.target_primaries != old.target_primaries;
if (heavy)
self->post_refresh(true);
else if (now.max != old.max || now.reference != old.reference ||
now.target_max != old.target_max)
self->post_refresh(false);
// Only a variant still wanted is described anew: an ICC description
// brings no luminances, and zeros are a fatal protocol error.
self->sync_variants(false);
if ((self->hdr_.object || self->hdr_pending_) &&
(now.reference != old.reference || now.target_max != old.target_max ||
now.target_min != old.target_min))
self->describe_hdr();
}
const wl_registry_listener WaylandColorBridge::kRegistryListener = {
@@ -166,10 +540,10 @@ const wl_registry_listener WaylandColorBridge::kRegistryListener = {
const wp_color_manager_v1_listener WaylandColorBridge::kManagerListener = {
.supported_intent = ignore_u32,
.supported_feature = ignore_u32,
.supported_tf_named = ignore_u32,
.supported_primaries_named = ignore_u32,
.done = ignore_done,
.supported_feature = supported_feature,
.supported_tf_named = supported_tf_named,
.supported_primaries_named = supported_primaries_named,
.done = manager_done,
};
const wp_color_management_surface_feedback_v1_listener
@@ -183,13 +557,28 @@ const wp_image_description_v1_listener
.ready = description_ready,
};
const wp_image_description_info_v1_listener WaylandColorBridge::kInfoListener =
{
.done = info_done,
.icc_file = info_icc_file,
.primaries = info_ignore_primaries,
.primaries_named = info_ignore_u32,
.tf_power = info_ignore_u32,
.tf_named = info_ignore_u32,
.luminances = info_luminances,
.target_primaries = info_target_primaries,
.target_luminance = info_target_luminance,
.target_max_cll = info_ignore_u32,
.target_max_fall = info_ignore_u32,
};
WaylandColorBridge::~WaylandColorBridge()
{
detach();
}
void
WaylandColorBridge::attach(QWindow *window)
WaylandColorBridge::attach(Window *window)
{
detach();
if (!window || QGuiApplication::platformName() != QStringLiteral("wayland"))
@@ -220,8 +609,15 @@ WaylandColorBridge::attach(QWindow *window)
void
WaylandColorBridge::detach()
{
if (this->pending_description_)
wp_image_description_v1_destroy(this->pending_description_);
if (this->pending_info_)
wp_image_description_info_v1_destroy(this->pending_info_);
for (auto *description : {this->preferred_, this->icc_pending_,
this->sdr_pending_, this->hdr_pending_})
if (description)
wp_image_description_v1_destroy(description);
drop(this->encoded_);
drop(this->sdr_);
drop(this->hdr_);
if (this->feedback_)
wp_color_management_surface_feedback_v1_destroy(this->feedback_);
if (this->color_surface_)
@@ -238,7 +634,49 @@ WaylandColorBridge::detach()
this->manager_ = nullptr;
this->color_surface_ = nullptr;
this->feedback_ = nullptr;
this->pending_description_ = nullptr;
this->preferred_ = nullptr;
this->pending_info_ = nullptr;
this->icc_pending_ = nullptr;
this->sdr_pending_ = nullptr;
this->hdr_pending_ = nullptr;
this->icc_supported_ = this->parametric_ = this->set_primaries_ =
this->set_luminances_ = this->ext_linear_ = this->bt2020_ = false;
this->icc_.clear();
this->unmatched_ = this->encoded_icc_ = this->hdr_stale_ = false;
this->describing_ = false;
this->primaries_ = {};
this->pending_ = this->info_ = {};
this->output_ = {};
this->latched_ = {};
this->applied_ = 0;
}
void
WaylandColorBridge::set_screen(
vector<uint8_t> icc, const dawn::ProfileEncoding *extended)
{
if (icc != this->icc_) {
this->icc_ = std::move(icc);
if (this->manager_ && this->icc_supported_ && this->output_.range.hdr)
describe_icc();
}
// Adapted, the colourants sum to D65, so the compositor's RGB-to-XYZ
// is ours without any adaptation of its own.
array<int32_t, 8> primaries{0, 0, 0, 0, 0, 0, 312700, 329000};
if (extended) {
const dawn::RgbMatrix m = dawn::display_colourants_d65(*extended);
for (int c = 0; c < 3; c++) {
const double sum = m[c][0] + m[c][1] + m[c][2];
primaries[c * 2] = int32_t(lround(m[c][0] / sum * 1e6));
primaries[c * 2 + 1] = int32_t(lround(m[c][1] / sum * 1e6));
}
}
const bool replace = primaries != this->primaries_;
this->describing_ = extended;
this->primaries_ = primaries;
if (this->manager_)
sync_variants(replace);
}
} // namespace dn
+114 -6
View File
@@ -8,45 +8,138 @@
#pragma once
#include "color-management-v1-client-protocol.h"
#include "display-profile.hpp"
#include "renderer.hpp"
#include <wayland-client.h>
class QWindow;
#include <array>
#include <cstdint>
#include <vector>
namespace dn
{
class Window;
/// What the compositor tells of the output, and of itself.
struct WaylandOutput {
DisplayRange range;
/// Parametric descriptions with luminances, usable primaries,
/// and the extended linear transfer function.
bool extended = false;
/// ICC descriptions (icc_v2_v4), without which a surface can only be
/// described parametrically, or not at all.
bool icc = false;
/// Parametric descriptions take our own primaries, not BT.2020's.
bool own_primaries = false;
};
class WaylandColorBridge
{
QWindow *window_ = nullptr;
// A description the compositor has made ready, kept for switching to.
struct Described {
wp_image_description_v1 *object = nullptr;
uint64_t id = 0; ///< Zero for none, which is no description
float white = 1;
};
// The preferred description's information, as its events bring it.
struct Info {
uint32_t min = 0, max = 0, reference = 0;
uint32_t target_min = 0, target_max = 0;
std::array<int32_t, 8> target_primaries{};
bool parametric = false;
bool have_target_primaries = false;
};
Window *window_ = nullptr;
wl_display *display_ = nullptr;
wl_surface *surface_ = nullptr;
wl_registry *registry_ = nullptr;
wp_color_manager_v1 *manager_ = nullptr;
wp_color_management_surface_v1 *color_surface_ = nullptr;
wp_color_management_surface_feedback_v1 *feedback_ = nullptr;
wp_image_description_v1 *pending_description_ = nullptr;
wp_image_description_v1 *preferred_ = nullptr;
wp_image_description_info_v1 *pending_info_ = nullptr;
std::vector<uint8_t> icc_;
bool icc_supported_ = false;
bool parametric_ = false;
bool set_primaries_ = false;
bool set_luminances_ = false;
bool ext_linear_ = false;
bool bt2020_ = false;
bool unmatched_ = false; ///< The ICC description failed
Info pending_, info_;
WaylandOutput output_;
// Encoded frames go out under the ICC description on an HDR output,
// the preferred description otherwise, or none. Extended frames take
// a parametric variant: SDR declares nothing above SDR white.
Described encoded_, sdr_, hdr_;
wp_image_description_v1 *icc_pending_ = nullptr;
wp_image_description_v1 *sdr_pending_ = nullptr;
wp_image_description_v1 *hdr_pending_ = nullptr;
float hdr_pending_white_ = 1;
bool hdr_stale_ = false; ///< Newer luminances await hdr_pending_
bool encoded_icc_ = false;
uint64_t next_id_ = 1;
// The screen profile's colourants, when the window can present extended.
bool describing_ = false;
std::array<int32_t, 8> primaries_{};
Described latched_;
uint64_t applied_ = 0;
void request_preferred();
void apply_description(wp_image_description_v1 *description);
void describe_icc();
wp_image_description_v1 *describe_parametric(uint32_t peak);
void describe_hdr();
void sync_variants(bool replace);
void drop(Described &described);
void set_unmatched(bool unmatched);
void post_refresh(bool heavy);
void ready(Described &slot, wp_image_description_v1 *object, float white);
static void registry_global(void *data, wl_registry *registry,
uint32_t name, const char *interface, uint32_t);
static void registry_remove(void *, wl_registry *, uint32_t);
static void ignore_u32(void *, wp_color_manager_v1 *, uint32_t);
static void ignore_done(void *, wp_color_manager_v1 *);
static void supported_feature(
void *data, wp_color_manager_v1 *, uint32_t feature);
static void supported_tf_named(
void *data, wp_color_manager_v1 *, uint32_t tf);
static void supported_primaries_named(
void *data, wp_color_manager_v1 *, uint32_t primaries);
static void manager_done(void *data, wp_color_manager_v1 *);
static void preferred_changed(
void *data, wp_color_management_surface_feedback_v1 *, uint32_t);
static void description_failed(void *data,
wp_image_description_v1 *description, uint32_t, const char *message);
static void description_ready(
void *data, wp_image_description_v1 *description, uint32_t);
static void info_icc_file(
void *, wp_image_description_info_v1 *, int32_t fd, uint32_t);
static void info_ignore_primaries(void *, wp_image_description_info_v1 *,
int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t);
static void info_target_primaries(void *data,
wp_image_description_info_v1 *, int32_t r_x, int32_t r_y, int32_t g_x,
int32_t g_y, int32_t b_x, int32_t b_y, int32_t w_x, int32_t w_y);
static void info_ignore_u32(
void *, wp_image_description_info_v1 *, uint32_t);
static void info_luminances(void *data, wp_image_description_info_v1 *,
uint32_t min_lum, uint32_t max_lum, uint32_t reference_lum);
static void info_target_luminance(void *data,
wp_image_description_info_v1 *, uint32_t min_lum, uint32_t max_lum);
static void info_done(void *data, wp_image_description_info_v1 *info);
static const wl_registry_listener kRegistryListener;
static const wp_color_manager_v1_listener kManagerListener;
static const wp_color_management_surface_feedback_v1_listener
kFeedbackListener;
static const wp_image_description_v1_listener kDescriptionListener;
static const wp_image_description_info_v1_listener kInfoListener;
public:
WaylandColorBridge() = default;
@@ -55,8 +148,23 @@ public:
WaylandColorBridge(const WaylandColorBridge &) = delete;
WaylandColorBridge &operator=(const WaylandColorBridge &) = delete;
void attach(QWindow *window);
void attach(Window *window);
void detach();
/// The profile our pixels are encoded in, and its colourants when the
/// window can present extended range, in which case they get
/// parametric descriptions too.
void set_screen(
std::vector<uint8_t> icc, const dawn::ProfileEncoding *extended);
/// The compositor reads our pixels as sRGB, whatever set_screen() said.
[[nodiscard]] bool unmatched() const { return this->unmatched_; }
[[nodiscard]] const WaylandOutput &output() const { return this->output_; }
/// Picks the description a frame goes out under, among those ready,
/// and returns the presentation that allows, with its SDR white.
Presentation latch(Presentation wanted, float *white);
/// Applies the latched description, right before the frame's present.
void apply_latched();
};
// True when the compositor does not advertise zxdg_decoration_manager_v1
+4
View File
@@ -56,6 +56,10 @@ public:
~WaylandWindow() override;
bool initialize(const QUrl &url, BrowseSetup setup, Mode mode);
[[nodiscard]] WaylandColorBridge &color_bridge()
{
return *this->color_bridge_;
}
QAccessibleInterface *accessibleRoot() const override;
};
+201 -16
View File
@@ -20,6 +20,7 @@
#include "window-appearance.hpp"
#if DN_WITH_WAYLAND
#include "wayland-color-bridge.hpp"
#include "wayland-window.hpp"
#endif
@@ -70,6 +71,7 @@
#include <cmath>
#include <functional>
#include <numbers>
#include <optional>
using namespace std;
@@ -219,6 +221,7 @@ Window::Window(App *app, QWindow *parent) : QWindow(parent), app_(app)
Window::~Window()
{
this->screen_parameters_.reset();
if (this->app_) {
this->app_->display_profiles.unlisten(this);
this->app_->settings.unlisten(this);
@@ -274,7 +277,13 @@ Window::initialize(const QUrl &url, BrowseSetup setup, Mode mode)
if (!this->renderer_.init(
this->app_->gpu, this->surface_, pixel_size(),
parent() ? VK_PRESENT_MODE_MAILBOX_KHR : VK_PRESENT_MODE_FIFO_KHR,
[this, instance] { instance->presentAboutToBeQueued(this); },
[this, instance] {
instance->presentAboutToBeQueued(this);
#if DN_WITH_WAYLAND
if (auto *shell = dynamic_cast<WaylandWindow *>(parent()))
shell->color_bridge().apply_latched();
#endif
},
[this, instance] { instance->presentQueued(this); }))
return false;
this->renderer_ready_ = true;
@@ -282,6 +291,9 @@ Window::initialize(const QUrl &url, BrowseSetup setup, Mode mode)
this->cmm_ = dawn::Cmm::get_default();
this->app_->display_profiles.listen(
this, [this] { handle_screen_change(screen()); });
// EDR headroom changes, and ramps, which the profile source does not see.
this->screen_parameters_ =
macos_watch_screen_parameters([this] { refresh_headroom(); });
this->app_->settings.listen(this, [this](SettingsChange change) {
if (change == SettingsChange::Preferences) {
this->renderer_.set_dither_enabled(
@@ -739,43 +751,165 @@ Window::shutdown()
this->surface_ = VK_NULL_HANDLE;
}
// Extended presentation needs the platform's description of it, a surface
// format for it, and a matrix/TRC working space. The order of profiles is:
// the override, the display's ICC, the output's own primaries, and sRGB.
// Under Windows Advanced Color, Windows decides how our pixels are read:
// the override does not apply, and a window that can present extended
// takes the output's primaries, since its every frame is extended.
bool
Window::refresh_screen_profile(QScreen *target_screen)
{
if (!this->cmm_)
this->cmm_ = dawn::Cmm::get_default();
DisplayRange range;
bool platform = false, icc = true, own_primaries = false;
float sdr_white = 1;
optional<DisplayProfile> discovered;
auto discover = [&]() -> const DisplayProfile & {
if (!discovered)
discovered = this->app_->display_profiles.load(target_screen);
return *discovered;
};
#if DN_WITH_WAYLAND
auto *shell = dynamic_cast<WaylandWindow *>(parent());
if (shell) {
const WaylandOutput &output = shell->color_bridge().output();
range = output.range;
platform = output.extended;
icc = output.icc;
own_primaries = output.own_primaries;
}
#endif
#ifdef Q_OS_WIN
// A profile from the legacy getters under Advanced Color is that of the
// compatibility helper, with which Windows would convert us twice.
this->advanced_color_ = discover().advanced_color;
const bool system_managed = this->advanced_color_.active;
platform = system_managed && discover().icc.empty();
range = this->advanced_color_.range;
sdr_white = this->advanced_color_.white;
#else
const bool system_managed = false;
#endif
#ifdef Q_OS_MACOS
platform = true;
range = macos_display_range(target_screen);
#endif
const bool offers_extended = platform && this->renderer_.offers_extended();
// Without ICC tagging, a profile is only of use to the present pass.
const bool parametric_only = range.hdr && !icc;
auto usable = [&](shared_ptr<dawn::Profile> profile) {
if (profile && parametric_only &&
!profile_encoding(profile.get()).matrix_trc)
return shared_ptr<dawn::Profile>();
return profile;
};
shared_ptr<dawn::Profile> next;
string label = "sRGB (fallback)";
string source = "srgb";
const vector<unsigned char> &override =
this->app_->settings.icc_profile_override;
if (!override.empty()) {
next = this->cmm_->get_profile(override);
if (!override.empty() && !system_managed) {
next = usable(this->cmm_->get_profile(override));
if (next) {
label = this->app_->settings.icc_profile_override_path;
source = "configuration";
} else {
qWarning(
"configuration dn/ICCProfileOverride: invalid ICC profile");
qWarning("configuration dn/ICCProfileOverride: invalid or "
"unusable ICC profile");
}
}
if (!next) {
DisplayProfile discovered =
this->app_->display_profiles.load(target_screen);
if (!discovered.icc.empty()) {
next = this->cmm_->get_profile(discovered.icc);
if (next) {
label = discovered.label.empty() ? discovered.source
: discovered.label;
source = discovered.source;
}
if (!next && !discover().icc.empty()) {
next = usable(this->cmm_->get_profile(discover().icc));
if (next) {
label =
discover().label.empty() ? discover().source : discover().label;
source = discover().source;
}
}
// The output's own primaries. Under Advanced Color, any working space
// that covers the display converts exactly to scRGB, and this one never
// meets an integer swapchain, which Windows reads as sRGB; the legacy
// getters return no ICC then. On Wayland, they are the display's actual
// gamut, not the BT.2020 container of PQ.
#ifdef Q_OS_WIN
const bool own_gamut = offers_extended;
const char *own_label = "DXGI output primaries", *own_source = "dxgi";
#else
const bool own_gamut = range.hdr;
const char *own_label = "the compositor's target primaries",
*own_source = "compositor";
#endif
if (!next && own_gamut && range.primaries) {
const auto &xy = *range.primaries;
const double white_point[2] = {xy[6], xy[7]};
next =
this->cmm_->get_profile_parametric(nullopt, white_point, xy.data());
if (next) {
label = own_label;
source = own_source;
}
}
if (!next)
next = this->cmm_->get_profile_sRGB();
#if DN_WITH_WAYLAND
// A compositor that failed to take the profile reads our pixels as sRGB,
// and extended frames must then say sRGB as well. It is still offered
// the profile, as other bytes would only have it try again.
vector<uint8_t> described;
if (shell) {
described = next->to_bytes();
if (shell->color_bridge().unmatched()) {
next = this->cmm_->get_profile_sRGB();
label = N_("sRGB (the compositor cannot take the display profile)");
source = "srgb";
}
}
#endif
const dawn::ProfileEncoding encoding = profile_encoding(next.get());
const bool capable = offers_extended && encoding.matrix_trc;
if (parametric_only && !capable) {
next = this->cmm_->get_profile_sRGB();
label = N_("sRGB (extended range unavailable)");
source = "srgb";
}
#if DN_WITH_WAYLAND
if (shell)
shell->color_bridge().set_screen(
std::move(described), capable ? &encoding : nullptr);
#endif
this->screen_profile_fallback_ = source == "srgb";
this->screen_state_.capable = capable;
this->screen_state_.hdr = range.hdr;
this->screen_state_.headroom = range.headroom;
PresentationTarget target;
target.capable = capable;
// Windows composes in scRGB anyway, and without ICC tagging on Wayland,
// encoded presentation could only leave the surface unmatched.
target.always = system_managed || parametric_only;
target.white = sdr_white;
if (capable && !own_primaries) {
#if defined Q_OS_WIN || defined Q_OS_MACOS
// Extended linear sRGB, which colours outside sRGB leave negative.
const double *primaries = dawn::kRec709Primaries;
#else
const double *primaries = dawn::kRec2020Primaries;
#endif
target.matrix = dawn::display_to_primaries(encoding, primaries);
}
#if DN_WITH_WAYLAND
if (shell)
target.latch = [shell](Presentation wanted, float *white) {
return shell->color_bridge().latch(wanted, white);
};
#endif
this->renderer_.set_presentation(std::move(target));
const bool changed =
!profiles_equal(this->screen_profile_.get(), next.get());
this->screen_profile_ = std::move(next);
@@ -1081,6 +1215,51 @@ Window::arm_ui_wake()
this->ui_wake_.stop();
}
// Only the headroom moved, or on Windows SDR white: no profile, no pipelines,
// no reload. Anything else the platform reports takes the heavy path.
void
Window::refresh_headroom()
{
#if DN_WITH_WAYLAND
if (auto *shell = dynamic_cast<WaylandWindow *>(parent()))
set_headroom(shell->color_bridge().output().range.headroom);
#endif
#ifdef Q_OS_WIN
// Nothing reports the SDR brightness slider, so activation re-reads it.
const AdvancedColor now = windows_advanced_color(screen());
const AdvancedColor &was = this->advanced_color_;
if (now.active != was.active || now.range.hdr != was.range.hdr ||
now.range.primaries != was.range.primaries) {
handle_screen_change(screen());
return;
}
this->advanced_color_ = now;
this->renderer_.set_white(now.white);
set_headroom(now.range.headroom);
request_render();
#endif
#ifdef Q_OS_MACOS
const DisplayRange range = macos_display_range(screen());
if (range.hdr != this->screen_state_.hdr) {
handle_screen_change(screen());
return;
}
set_headroom(range.headroom);
#endif
}
void
Window::set_headroom(float headroom)
{
if (headroom == this->screen_state_.headroom)
return;
this->screen_state_.headroom = headroom;
for (auto &page : this->pages_)
if (page && page->content)
page->content->screen_changed(this->screen_state_, false, false);
request_render();
}
void
Window::handle_screen_change(QScreen *target_screen)
{
@@ -1204,8 +1383,14 @@ Window::event(QEvent *event)
}
break;
}
case QEvent::FocusIn:
case QEvent::WindowActivate:
#ifdef Q_OS_WIN
// Once per activation, which also brings FocusIn.
if (this->renderer_ready_)
refresh_headroom();
#endif
[[fallthrough]];
case QEvent::FocusIn:
focus_gained();
break;
case QEvent::FocusOut:
+9 -1
View File
@@ -7,6 +7,7 @@
#pragma once
#include "display-profile.hpp"
#include "kit-browser.hpp"
#include "kit-chrome.hpp"
#include "kit-commander.hpp"
@@ -54,7 +55,6 @@ class Window final : public QWindow
void request_render();
void arm_ui_wake();
void render();
void handle_screen_change(QScreen *target_screen);
void focus_gained();
void focus_lost();
void begin_close();
@@ -111,6 +111,11 @@ class Window final : public QWindow
bool font_change_pending_ = false;
bool update_pending_ = false;
bool screen_profile_fallback_ = true;
// What the screen profile last saw of Windows Advanced Color, against
// which activation tells a brightness change from a mode change.
AdvancedColor advanced_color_;
// macOS screen-parameter notifications, for as long as this lives.
std::shared_ptr<void> screen_parameters_;
bool awaiting_view_ = false;
QTimer ui_wake_;
QTimer present_retry_;
@@ -153,6 +158,9 @@ public:
bool initialize(const QUrl &url, BrowseSetup setup, Mode mode);
void shutdown();
void handle_screen_change(QScreen *target_screen);
void refresh_headroom();
void set_headroom(float headroom);
void open_any(const QUrl &url);
void reveal_file(const QUrl &url);
Mode application() const { return application_mode(mode_); }
+7
View File
@@ -87,6 +87,13 @@ Therefore, the viewer processes transparent pages in encoded values by default,
and thumbnail production scales transparent images in encoded values.
Both scale fully opaque images in linear light.
The HDR rendition of an image is shown when it has a gain map, the display can
show it, and the toggle is on. It is always processed in linear light,
transparent or not, as nothing above SDR white has a convention for
compositing in encoded values. This holds even while the display's current
headroom leaves no gain at all, so that a change in brightness never changes
how transparency composites.
Thumbnails
----------
When loading a directory, retrieve thumbnails as fast as possible:
+111 -22
View File
@@ -461,6 +461,107 @@ profile_encoding(const Profile *profile)
return matrix;
}
// --- Matrices ----------------------------------------------------------------
// Column-indexed: the result's column c is `a` applied to `b`'s column c.
static RgbMatrix
rgb_multiply(const RgbMatrix &a, const RgbMatrix &b)
{
RgbMatrix m{};
for (size_t c = 0; c < 3; c++)
for (size_t r = 0; r < 3; r++)
for (size_t k = 0; k < 3; k++)
m[c][r] += a[k][r] * b[c][k];
return m;
}
static RgbMatrix
rgb_inverse(const RgbMatrix &m)
{
// With columns a, b, c, the rows of the inverse are b×c, c×a, a×b,
// over the determinant.
auto cross = [](const array<double, 3> &u, const array<double, 3> &v) {
return array<double, 3>{u[1] * v[2] - u[2] * v[1],
u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]};
};
const array<double, 3> rows[3] = {
cross(m[1], m[2]), cross(m[2], m[0]), cross(m[0], m[1])};
const double det =
m[0][0] * rows[0][0] + m[0][1] * rows[0][1] + m[0][2] * rows[0][2];
RgbMatrix inverse{};
for (size_t r = 0; r < 3; r++)
for (size_t c = 0; c < 3; c++)
inverse[c][r] = rows[r][c] / det;
return inverse;
}
static RgbMatrix
rgb_diagonal(double a, double b, double c)
{
return {{{a, 0, 0}, {0, b, 0}, {0, 0, c}}};
}
// The Bradford transform from the ICC PCS illuminant to D65.
static RgbMatrix
bradford_d50_to_d65()
{
// Rows 0.8951 0.2664 -0.1614, -0.7502 1.7135 0.0367,
// 0.0389 -0.0685 1.0296, stored by column.
static const RgbMatrix cone = {{{0.8951, -0.7502, 0.0389},
{0.2664, 1.7135, -0.0685}, {-0.1614, 0.0367, 1.0296}}};
const cmsCIEXYZ *d50 = cmsD50_XYZ();
auto response = [&](const cmsCIEXYZ &xyz) {
array<double, 3> lms{};
for (size_t r = 0; r < 3; r++)
lms[r] =
cone[0][r] * xyz.X + cone[1][r] * xyz.Y + cone[2][r] * xyz.Z;
return lms;
};
const auto from = response(*d50), to = response(kD65Xyz);
return rgb_multiply(rgb_inverse(cone),
rgb_multiply(
rgb_diagonal(to[0] / from[0], to[1] / from[1], to[2] / from[2]),
cone));
}
RgbMatrix
display_colourants_d65(const ProfileEncoding &encoding)
{
return rgb_multiply(bradford_d50_to_d65(), encoding.rgb_to_xyz);
}
// From linear RGB of CIE 1931 xy `primaries` with a D65 white to D65 XYZ.
static RgbMatrix
primaries_to_xyz_d65(const double primaries[6])
{
// Primaries scaled so that they sum to the white point.
RgbMatrix target{};
for (size_t c = 0; c < 3; c++) {
const double x = primaries[c * 2], y = primaries[c * 2 + 1];
target[c] = {x / y, 1, (1 - x - y) / y};
}
const RgbMatrix inverse = rgb_inverse(target);
array<double, 3> scale{};
for (size_t r = 0; r < 3; r++)
scale[r] = inverse[0][r] * kD65Xyz.X + inverse[1][r] * kD65Xyz.Y +
inverse[2][r] * kD65Xyz.Z;
return rgb_multiply(target, rgb_diagonal(scale[0], scale[1], scale[2]));
}
RgbMatrix
display_to_primaries(const ProfileEncoding &encoding, const double primaries[6])
{
return rgb_multiply(rgb_inverse(primaries_to_xyz_d65(primaries)),
display_colourants_d65(encoding));
}
RgbMatrix
primaries_to_primaries(const double from[6], const double to[6])
{
return rgb_multiply(
rgb_inverse(primaries_to_xyz_d65(to)), primaries_to_xyz_d65(from));
}
// --- Colour management -------------------------------------------------------
Cmm::Cmm()
@@ -642,25 +743,18 @@ Cmm::get_profile_tabulated(const double whitepoint[2],
shared_ptr<Profile>
Cmm::get_profile_sRGB_gamma(double gamma)
{
double wp[2] = {0.3127, 0.3290};
double prim[6] = {0.6400, 0.3300, 0.3000, 0.6000, 0.1500, 0.0600};
return get_profile_parametric(gamma, wp, prim);
return get_profile_parametric(gamma, kD65White, kRec709Primaries);
}
/// H.273 Table 2 primaries as CIE 1931 xy, in R,G,B order, plus the
/// illuminant. False for reserved, unspecified, or non-RGB code points.
static bool
bool
cicp_primaries(uint8_t code, double primaries[6], double whitepoint[2])
{
static const double kD65[2] = {0.3127, 0.3290};
const double *wp = kD65;
const double *wp = kD65White;
const double *p = nullptr;
switch (code) {
case 1: { // BT.709 / sRGB
static const double v[6] = {0.640, 0.330, 0.300, 0.600, 0.150, 0.060};
p = v;
case 1: // BT.709 / sRGB
p = kRec709Primaries;
break;
}
case 4: { // BT.470 System M, illuminant C
static const double v[6] = {0.670, 0.330, 0.210, 0.710, 0.140, 0.080};
static const double c[2] = {0.310, 0.316};
@@ -679,23 +773,18 @@ cicp_primaries(uint8_t code, double primaries[6], double whitepoint[2])
p = v;
break;
}
case 9: { // BT.2020 / BT.2100
static const double v[6] = {0.708, 0.292, 0.170, 0.797, 0.131, 0.046};
p = v;
case 9: // BT.2020 / BT.2100
p = kRec2020Primaries;
break;
}
case 11: { // SMPTE RP 431-2 (DCI-P3), DCI white
static const double v[6] = {0.680, 0.320, 0.265, 0.690, 0.150, 0.060};
static const double dci[2] = {0.314, 0.351};
p = v;
p = kP3Primaries;
wp = dci;
break;
}
case 12: { // SMPTE EG 432-1 (Display P3), D65 white
static const double v[6] = {0.680, 0.320, 0.265, 0.690, 0.150, 0.060};
p = v;
case 12: // SMPTE EG 432-1 (Display P3), D65 white
p = kP3Primaries;
break;
}
default:
// 0/3 reserved, 2 unspecified, 10 is XYZ, 22 has no ICC analogue.
return false;
+108
View File
@@ -75,6 +75,114 @@ bool inflate_raw(std::span<const uint8_t> src, std::span<uint8_t> dst);
/// Returns nothing when the offset does not fit the payload.
std::vector<uint8_t> iso_exif_payload(std::span<const uint8_t> payload);
// --- Gain maps ---------------------------------------------------------------
/// Values of an XMP property named by namespace URI and local name: as an
/// attribute, then as an element holding text or rdf:li items.
/// Not an XML parser, just enough for what cameras and encoders write.
std::vector<std::string> xmp_values(
std::string_view xmp, std::string_view ns, std::string_view name);
bool xmp_declares(std::string_view xmp, std::string_view ns);
/// ISO 21496-1 GainMapMetadata, as JPEG APP2 carries it past its namespace.
/// False when malformed, or when this is not what GainMap can represent:
/// channel-dependent values, unequal offsets. Only the metadata is filled.
bool parse_iso_gain_map(std::span<const uint8_t> blob, GainMap *map);
/// Ultra HDR `hdrgm` XMP metadata, likewise.
bool parse_hdrgm_gain_map(std::string_view xmp, GainMap *map);
/// The payload of a HEIF `tmap` item, likewise.
bool parse_tmap_gain_map(std::span<const uint8_t> item, GainMap *map);
/// The ISO 21496-1 metadata and the map's naked codestream in a JPEG XL
/// `jhgm` box. libjxl declares a reader, but does not export it.
bool split_jhgm_bundle(std::span<const uint8_t> box,
std::span<const uint8_t> *metadata, std::span<const uint8_t> *codestream);
/// Whether XMP uses Apple's HDRGainMap namespace, as its maps do.
bool apple_gain_map_declared(std::string_view xmp);
/// Linear headroom of Apple's pre-ISO map, from the map's own XMP, or from
/// the maker note in the primary image's Exif. Zero when unusable.
double apple_gain_map_headroom(
std::string_view map_xmp, std::span<const uint8_t> exif);
/// The same, with maker note tags 33 and 48 already read out, as NaN and 0
/// when missing.
double apple_gain_map_headroom_from_maker(
std::string_view map_xmp, double maker33, double maker48);
/// The metadata an Apple map is normalized to, see make_gain_map().
GainMap apple_gain_map(double headroom);
/// False for maps with no HDR effect, and those over an HDR base, which
/// also warn. Metadata that fails this needs no decoding.
bool gain_map_applies(const GainMap &metadata, const OpenContext &ctx);
/// One channel of the decoded map with the metadata, or null when its
/// channels differ. Apple's texels are rewritten to log2 gain.
std::unique_ptr<GainMap> make_gain_map(
const Image &pixels, const GainMap &metadata, bool apple);
/// Bring a map into its base's frame, where a container transforms that.
/// Cropping fails, leaving the map alone, when the rectangle does not fit.
bool crop_gain_map(
GainMap &map, uint32_t x, uint32_t y, uint32_t w, uint32_t h);
/// Counter-clockwise, by a multiple of 90 degrees.
void rotate_gain_map(GainMap &map, int ccw);
/// Left to right when `horizontally`, otherwise top to bottom.
void mirror_gain_map(GainMap &map, bool horizontally);
// --- True HDR ----------------------------------------------------------------
/// The headroom split_hdr() caps its HDR rendition at: a 1000 cd/m² alternate
/// at 203 cd/m² reference white (BT.2408).
inline constexpr double kSplitHeadroom = 1000. / 203.;
/// Rolls linear light in [0, peak] onto [0, target], the identity below a knee,
/// and everywhere when `peak <= target`. This is BT.2390's EETF, knee and
/// Hermite spline, but over log2 of linear light rather than over PQ, so that
/// the peak has no upper bound.
class HdrRolloff
{
double knee_ = 0; ///< log2, where the roll-off starts
double span_ = 0; ///< log2, from the knee to the peak
double threshold_ = 0; ///< Linear, the knee; infinite for the identity
public:
HdrRolloff(double peak, double target);
double apply(double x) const;
};
/// H.273 Table 2 primaries as CIE 1931 xy, in R,G,B order, plus the
/// illuminant. False for reserved, unspecified, or non-RGB code points.
bool cicp_primaries(uint8_t code, double primaries[6], double whitepoint[2]);
/// Splits linear light, 1.0 at SDR reference white, into an SDR base and,
/// when `ctx.gain_maps` is set and anything exceeds 1.0, a gain map to an
/// alternate capped at kSplitHeadroom. `rgba` holds four values per pixel of
/// `image`, in CIE 1931 xy `primaries` with a D65 white, alpha associated when
/// `premultiplied`, and serves as scratch. Visible negative values widen the
/// primaries first, see widen_negative(). Non-finite values, and negative
/// ones past that, warn, and become zero. The base goes into `image.data`
/// straight, encoded with the sRGB EOTF in those primaries, which
/// `image.effective_profile` then describes: finish_image() with that as its
/// source completes it.
bool split_hdr(Image &image, const OpenContext &ctx, std::span<float> rgba,
bool premultiplied, const double primaries[6], Error *error);
/// Where a visible pixel of straight `rgba` has a negative channel, converts
/// all of it from `primaries` to BT.2020's, which take in most of what goes
/// negative, and returns those. Otherwise returns `primaries`, and changes
/// nothing.
const double *widen_negative(std::span<float> rgba, const double primaries[6]);
/// Whether H.273 code points say PQ or HLG, in primaries that split_hdr()
/// takes: those of a D65 white, which go into `primaries`.
bool cicp_hdr(uint8_t primaries_code, uint8_t transfer, double primaries[6]);
/// Splits a decoded BT.2100 signal, PQ (16) or HLG (18), in `image.data`,
/// as split_hdr() does. PQ puts 1.0 at 203 cd/m² (BT.2408). HLG gets the
/// OOTF of a display with a peak of `hlg_peak` cd/m², and 1.0 goes to its
/// reference white, 75 % signal, which comes to 203 cd/m² at 1000.
bool split_hdr_signal(Image &image, const OpenContext &ctx, uint8_t transfer,
const double primaries[6], double hlg_peak, bool premultiplied,
Error *error);
/// MIME types compiled into the in-tree Rust decoder.
std::vector<std::string> dnrs_media_types();
+813
View File
@@ -13,11 +13,16 @@
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <initializer_list>
#include <limits>
#include <locale>
#include <new>
#include <numbers>
#include <sstream>
#include <string_view>
#ifdef _WIN32
@@ -1139,6 +1144,814 @@ exif_orientation(span<const uint8_t> exif)
return Orientation::Unknown;
}
// --- Gain maps ---------------------------------------------------------------
float
gain_map_weight(const GainMap &map, float headroom)
{
const float range = map.alternate_headroom - map.base_headroom;
if (!(headroom > 0) || !(range > 0))
return 0;
return clamp((log2(headroom) - map.base_headroom) / range, 0.f, 1.f);
}
static bool
xml_space(char c)
{
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
}
static string_view
xml_trim(string_view s)
{
while (!s.empty() && xml_space(s.front()))
s.remove_prefix(1);
while (!s.empty() && xml_space(s.back()))
s.remove_suffix(1);
return s;
}
// The prefix of the first xmlns declaration binding the namespace URI.
static string_view
xmp_prefix(string_view xmp, string_view ns)
{
for (size_t at = 0; (at = xmp.find(ns, at)) != string_view::npos;
at += ns.size()) {
const size_t end = at + ns.size();
if (at < 3 || end >= xmp.size() || xmp[end] != xmp[at - 1] ||
(xmp[at - 1] != '"' && xmp[at - 1] != '\''))
continue;
string_view before = xml_trim(xmp.substr(0, at - 1));
if (before.empty() || before.back() != '=')
continue;
before = xml_trim(before.substr(0, before.size() - 1));
const size_t name = before.find_last_of(" \t\r\n<");
string_view attribute =
name == string_view::npos ? before : before.substr(name + 1);
if (attribute.starts_with("xmlns:"))
return attribute.substr(6);
}
return {};
}
// Whether a qualified name ends at `at`, rather than continuing.
static bool
xml_name_ends(string_view xmp, size_t at)
{
return at >= xmp.size() || xml_space(xmp[at]) || xmp[at] == '=' ||
xmp[at] == '>' || xmp[at] == '/';
}
static void
xmp_attribute_values(string_view xmp, string_view qname, vector<string> &out)
{
for (size_t at = 0; (at = xmp.find(qname, at)) != string_view::npos;
at += qname.size()) {
size_t p = at + qname.size();
if (!at || !xml_space(xmp[at - 1]) || !xml_name_ends(xmp, p))
continue;
while (p < xmp.size() && xml_space(xmp[p]))
p++;
if (p >= xmp.size() || xmp[p++] != '=')
continue;
while (p < xmp.size() && xml_space(xmp[p]))
p++;
if (p >= xmp.size() || (xmp[p] != '"' && xmp[p] != '\''))
continue;
const size_t end = xmp.find(xmp[p], p + 1);
if (end == string_view::npos)
break;
out.emplace_back(xml_trim(xmp.substr(p + 1, end - p - 1)));
}
}
static void
xmp_element_values(string_view xmp, string_view qname, vector<string> &out)
{
const string open = "<" + string(qname), close = "</" + string(qname);
for (size_t at = 0; (at = xmp.find(open, at)) != string_view::npos;
at += open.size()) {
const size_t gt = xmp.find('>', at);
if (!xml_name_ends(xmp, at + open.size()) || gt == string_view::npos ||
xmp[gt - 1] == '/')
continue;
const size_t end = xmp.find(close, gt);
if (end == string_view::npos)
break;
string_view content = xmp.substr(gt + 1, end - gt - 1);
if (content.find('<') == string_view::npos) {
out.emplace_back(xml_trim(content));
continue;
}
for (size_t li = 0;
(li = content.find("<rdf:li", li)) != string_view::npos; li++) {
const size_t li_gt = content.find('>', li);
const size_t li_end = content.find("</rdf:li>", li);
if (li_gt == string_view::npos || li_end == string_view::npos ||
li_gt > li_end)
break;
out.emplace_back(
xml_trim(content.substr(li_gt + 1, li_end - li_gt - 1)));
}
}
}
vector<string>
xmp_values(string_view xmp, string_view ns, string_view name)
{
vector<string> values;
string_view prefix = xmp_prefix(xmp, ns);
if (prefix.empty())
return values;
const string qname = string(prefix) + ":" + string(name);
xmp_attribute_values(xmp, qname, values);
xmp_element_values(xmp, qname, values);
return values;
}
bool
xmp_declares(string_view xmp, string_view ns)
{
return !xmp_prefix(xmp, ns).empty();
}
static bool
parse_decimal(string_view text, double *out)
{
istringstream in{string(text)};
in.imbue(locale::classic());
return in >> *out && in.eof() && isfinite(*out);
}
// ISO 21496-1 metadata is only usable with channel-independent values.
static bool
single_value(const vector<double> &values, double *out)
{
if (values.empty())
return false;
for (double value : values)
if (value != values.front())
return false;
*out = values.front();
return true;
}
static bool
checked_gain_map(GainMap *map, double min, double max, double gamma,
double base_offset, double alternate_offset, double base_headroom,
double alternate_headroom)
{
if (!(gamma > 0) || !(max >= min) || base_offset != alternate_offset)
return false;
map->min = float(min);
map->max = float(max);
map->gamma = float(gamma);
map->offset = float(base_offset);
map->base_headroom = float(base_headroom);
map->alternate_headroom = float(alternate_headroom);
return true;
}
namespace
{
struct BigEndianReader {
const uint8_t *p, *end;
bool u32(uint32_t *out)
{
if (end - p < 4)
return false;
*out = uint32_t(p[0]) << 24 | uint32_t(p[1]) << 16 |
uint32_t(p[2]) << 8 | p[3];
p += 4;
return true;
}
bool fraction(bool is_signed, uint32_t denominator, double *out)
{
uint32_t n = 0;
if (!u32(&n) || !denominator)
return false;
*out = (is_signed ? double(int32_t(n)) : double(n)) / denominator;
return true;
}
bool rational(bool is_signed, double *out)
{
uint32_t n = 0, d = 0;
if (!u32(&n) || !u32(&d) || !d)
return false;
*out = (is_signed ? double(int32_t(n)) : double(n)) / d;
return true;
}
};
} // namespace
bool
parse_iso_gain_map(span<const uint8_t> blob, GainMap *map)
{
// minimum_version, writer_version, then the flags. The primary image
// of a JPEG carries only the versions.
if (blob.size() < 5 || blob[0] || blob[1])
return false;
const uint8_t flags = blob[4];
const bool multichannel = flags & 0x80;
const bool backward = flags & 0x04;
const bool common = flags & 0x08;
BigEndianReader r{blob.data() + 5, blob.data() + blob.size()};
uint32_t denominator = 0;
double base_headroom = 0, alternate_headroom = 0;
if (common) {
if (!r.u32(&denominator) ||
!r.fraction(false, denominator, &base_headroom) ||
!r.fraction(false, denominator, &alternate_headroom))
return false;
} else if (!r.rational(false, &base_headroom) ||
!r.rational(false, &alternate_headroom)) {
return false;
}
vector<double> values[5];
for (int c = 0; c < (multichannel ? 3 : 1); c++) {
for (int i = 0; i < 5; i++) {
const bool is_signed = i != 2; // min, max, gamma, offsets
double v = 0;
if (common ? !r.fraction(is_signed, denominator, &v)
: !r.rational(is_signed, &v))
return false;
values[i].push_back(v);
}
}
double v[5] = {};
for (int i = 0; i < 5; i++)
if (!single_value(values[i], &v[i]))
return false;
// The backward direction has an HDR base, whatever the headrooms say.
if (backward && base_headroom <= alternate_headroom)
swap(base_headroom, alternate_headroom);
return checked_gain_map(
map, v[0], v[1], v[2], v[3], v[4], base_headroom, alternate_headroom);
}
bool
parse_tmap_gain_map(span<const uint8_t> item, GainMap *map)
{
// The item puts a version of its own in front, and only 0 is defined.
return !item.empty() && !item[0] &&
parse_iso_gain_map(item.subspan(1), map);
}
bool
split_jhgm_bundle(span<const uint8_t> box, span<const uint8_t> *metadata,
span<const uint8_t> *codestream)
{
// As libjxl's lib/extras/gain_map.cc lays it out, big-endian: version,
// the metadata, the alternate's colour encoding and ICC profile, which
// a map applied in display space has no use for, then the codestream.
if (box.size() < 3 || box[0])
return false;
const size_t metadata_size = size_t(box[1]) << 8 | box[2];
size_t at = 3;
if (box.size() - at < metadata_size + 1)
return false;
*metadata = box.subspan(at, metadata_size);
at += metadata_size;
const size_t encoding_size = box[at++];
if (box.size() - at < encoding_size + 4)
return false;
at += encoding_size;
const size_t icc_size = size_t(box[at]) << 24 | size_t(box[at + 1]) << 16 |
size_t(box[at + 2]) << 8 | box[at + 3];
at += 4;
if (box.size() - at <= icc_size)
return false;
*codestream = box.subspan(at + icc_size);
return true;
}
bool
parse_hdrgm_gain_map(string_view xmp, GainMap *map)
{
static constexpr string_view ns = "http://ns.adobe.com/hdr-gain-map/1.0/";
auto get = [&](string_view name, double fallback, double *out) {
vector<string> texts = xmp_values(xmp, ns, name);
if (texts.empty()) {
*out = fallback;
return !isnan(fallback);
}
if (texts.size() != 1 && texts.size() != 3)
return false;
vector<double> values(texts.size());
for (size_t i = 0; i < texts.size(); i++)
if (!parse_decimal(texts[i], &values[i]))
return false;
return single_value(values, out);
};
// GainMapMax and HDRCapacityMax have no defaults.
const double required = NAN, offset = 1. / 64;
double min = 0, max = 0, gamma = 0, offset_sdr = 0, offset_hdr = 0,
capacity_min = 0, capacity_max = 0;
if (!get("GainMapMin", 0, &min) || !get("GainMapMax", required, &max) ||
!get("Gamma", 1, &gamma) || !get("OffsetSDR", offset, &offset_sdr) ||
!get("OffsetHDR", offset, &offset_hdr) ||
!get("HDRCapacityMin", 0, &capacity_min) ||
!get("HDRCapacityMax", required, &capacity_max))
return false;
vector<string> hdr_base = xmp_values(xmp, ns, "BaseRenditionIsHDR");
if (!hdr_base.empty() && hdr_base.front() == "True")
return checked_gain_map(map, min, max, gamma, offset_hdr, offset_sdr,
capacity_max, capacity_min);
return checked_gain_map(map, min, max, gamma, offset_sdr, offset_hdr,
capacity_min, capacity_max);
}
static constexpr string_view kAppleGainMapNs =
"http://ns.apple.com/HDRGainMap/1.0/";
bool
apple_gain_map_declared(string_view xmp)
{
return xmp_declares(xmp, kAppleGainMapNs);
}
// Tags 33 and 48 of Apple's maker note, which is an IFD at offset 14,
// preceded by "Apple iOS", a version, and a byte order mark, with offsets
// relative to its own beginning. Tags it lacks are left alone.
static void
apple_maker_note_tags(
span<const uint8_t> note, double *maker33, double *maker48)
{
if (note.size() < 16 || memcmp(note.data(), "Apple iOS\0", 10))
return;
struct tiffer T = {};
T.begin = note.data();
T.end = note.data() + note.size();
if (!memcmp(note.data() + 12, "MM", 2))
T.un = &tiffer_unbe;
else if (!memcmp(note.data() + 12, "II", 2))
T.un = &tiffer_unle;
else
return;
T.p = note.data() + 14;
if (!tiffer_u16(&T, &T.remaining_fields))
return;
struct tiffer_entry entry = {};
while (tiffer_next_entry(&T, &entry)) {
if (entry.tag == 33 && entry.remaining_count == 1)
(void) tiffer_real(&T, &entry, maker33);
else if (entry.tag == 48 && entry.remaining_count == 1)
(void) tiffer_real(&T, &entry, maker48);
}
}
// Zero when the tags do not make for a headroom.
static double
apple_maker_headroom(double maker33, double maker48)
{
if (!isfinite(maker33) || !isfinite(maker48))
return 0;
// https://developer.apple.com/documentation/appkit/applying-apple-hdr-effect-to-your-photos
double stops = 0;
if (maker33 < 1)
stops =
maker48 <= 0.01 ? -20 * maker48 + 1.8 : -0.101 * maker48 + 1.601;
else
stops = maker48 <= 0.01 ? -70 * maker48 + 3 : -0.303 * maker48 + 2.303;
return exp2(max(stops, 0.));
}
static span<const uint8_t>
exif_maker_note(span<const uint8_t> exif)
{
struct tiffer T = {};
if (!tiffer_init(&T, exif.data(), exif.size()) || !tiffer_next_ifd(&T))
return {};
struct tiffer_entry entry = {};
int64_t offset = -1;
while (tiffer_next_entry(&T, &entry))
if (entry.tag == TIFF_ExifIFDPointer && entry.remaining_count == 1)
(void) tiffer_integer(&T, &entry, &offset);
struct tiffer subT = {};
if (offset < 0 || offset > UINT32_MAX ||
!tiffer_subifd(&T, uint32_t(offset), &subT))
return {};
while (tiffer_next_entry(&subT, &entry))
if (entry.tag == Exif_MakerNote && entry.type == TIFFER_UNDEFINED)
return span<const uint8_t>(entry.p, entry.remaining_count);
return {};
}
double
apple_gain_map_headroom_from_maker(
string_view map_xmp, double maker33, double maker48)
{
if (xmp_values(map_xmp, kAppleGainMapNs, "HDRGainMapVersion").empty())
return 0;
double headroom = 0;
vector<string> stored =
xmp_values(map_xmp, kAppleGainMapNs, "HDRGainMapHeadroom");
if (!stored.empty()) {
if (!parse_decimal(stored.front(), &headroom))
return 0;
} else {
headroom = apple_maker_headroom(maker33, maker48);
}
return headroom >= 1 ? headroom : 0;
}
double
apple_gain_map_headroom(string_view map_xmp, span<const uint8_t> exif)
{
double maker33 = NAN, maker48 = 0;
apple_maker_note_tags(exif_maker_note(exif), &maker33, &maker48);
return apple_gain_map_headroom_from_maker(map_xmp, maker33, maker48);
}
GainMap
apple_gain_map(double headroom)
{
GainMap map;
map.max = map.alternate_headroom = float(log2(headroom));
return map;
}
bool
gain_map_applies(const GainMap &metadata, const OpenContext &ctx)
{
if (metadata.base_headroom > metadata.alternate_headroom) {
add_warning(ctx, _("Gain maps over an HDR base are not supported"));
return false;
}
return metadata.alternate_headroom != metadata.base_headroom &&
(metadata.min != 0 || metadata.max != 0);
}
// The inverse of the Rec. 709 transfer function.
static double
rec709_to_linear(double v)
{
return v < 0.081 ? v / 4.5 : pow((v + 0.099) / 1.099, 1 / 0.45);
}
unique_ptr<GainMap>
make_gain_map(const Image &pixels, const GainMap &metadata, bool apple)
{
if (!pixels.width || !pixels.height)
return nullptr;
auto map = make_unique<GainMap>(metadata);
map->width = pixels.width;
map->height = pixels.height;
map->data.resize(size_t(pixels.width) * pixels.height);
uint16_t *out = map->data.data();
for (uint32_t y = 0; y < pixels.height; y++) {
const uint16_t *p = row_u16(pixels, y);
for (uint32_t x = 0; x < pixels.width; x++, p += 4) {
if (p[0] != p[1] || p[1] != p[2])
return nullptr;
*out++ = p[0];
}
}
if (!apple)
return map;
// Apple's gain is linear in the linearized texel. Rewritten as
// normalized log2 gain, the renderer knows just the one formula.
const double headroom = exp2(double(metadata.max));
vector<uint16_t> lut(65536);
for (uint32_t i = 0; i < lut.size(); i++) {
const double v = rec709_to_linear(i / 65535.);
lut[i] = uint16_t(
lround(65535 * log2(1 + (headroom - 1) * v) / log2(headroom)));
}
for (uint16_t &texel : map->data)
texel = lut[texel];
return map;
}
bool
crop_gain_map(GainMap &map, uint32_t x, uint32_t y, uint32_t w, uint32_t h)
{
if (!w || !h || x > map.width || w > map.width - x || y > map.height ||
h > map.height - y)
return false;
vector<uint16_t> data(size_t(w) * h);
for (uint32_t row = 0; row < h; row++) {
const uint16_t *src = map.data.data() + size_t(y + row) * map.width + x;
copy(src, src + w, data.begin() + ptrdiff_t(size_t(row) * w));
}
map.data = std::move(data);
map.width = w;
map.height = h;
return true;
}
void
rotate_gain_map(GainMap &map, int ccw)
{
ccw = ((ccw % 360) + 360) % 360;
if (!ccw)
return;
const uint32_t w = map.width, h = map.height;
const bool quarter = ccw != 180;
const uint32_t out_w = quarter ? h : w;
vector<uint16_t> data(map.data.size());
for (uint32_t y = 0; y < h; y++)
for (uint32_t x = 0; x < w; x++) {
uint32_t ox = 0, oy = 0;
if (ccw == 90)
ox = y, oy = w - 1 - x;
else if (ccw == 180)
ox = w - 1 - x, oy = h - 1 - y;
else
ox = h - 1 - y, oy = x;
data[size_t(oy) * out_w + ox] = map.data[size_t(y) * w + x];
}
map.data = std::move(data);
if (quarter)
swap(map.width, map.height);
}
void
mirror_gain_map(GainMap &map, bool horizontally)
{
const uint32_t w = map.width, h = map.height;
if (horizontally) {
for (uint32_t y = 0; y < h; y++) {
auto row = map.data.begin() + ptrdiff_t(size_t(y) * w);
reverse(row, row + w);
}
return;
}
for (uint32_t y = 0; y < h / 2; y++)
swap_ranges(map.data.begin() + ptrdiff_t(size_t(y) * w),
map.data.begin() + ptrdiff_t(size_t(y + 1) * w),
map.data.begin() + ptrdiff_t(size_t(h - 1 - y) * w));
}
// --- True HDR ----------------------------------------------------------------
HdrRolloff::HdrRolloff(double peak, double target)
{
threshold_ = numeric_limits<double>::infinity();
if (!(peak > target) || !(target > 0))
return;
// BT.2390 puts the knee at 1.5·T − 0.5 of the normalized source range,
// which leaves the spline a third of the span to roll off, the least
// that keeps it monotonic. With that, the spline is a cubic.
const double top = log2(peak), bottom = log2(target);
span_ = 1.5 * (top - bottom);
knee_ = top - span_;
threshold_ = exp2(knee_);
}
double
HdrRolloff::apply(double x) const
{
if (!(x > threshold_))
return x;
const double t = min((log2(x) - knee_) / span_, 1.);
return exp2(knee_ + span_ * (1 - pow(1 - t, 3)) / 3);
}
// Makes a pixel what split_hdr() takes: finite, not negative, and black when
// invisible, noting what had to be replaced.
static void
sanitize_hdr_pixel(float *p, bool &nonfinite, bool &negative)
{
if (!isfinite(p[3])) {
nonfinite = true;
p[3] = p[3] > 0 ? 1 : 0;
}
p[3] = clamp(p[3], 0.f, 1.f);
// An invisible pixel must not attach a map or raise the peak,
// whatever it holds.
if (p[3] == 0) {
p[0] = p[1] = p[2] = 0;
return;
}
// Conversions into wider primaries leave rounding below zero, which is
// neither visible nor worth a message at this fraction of the pixel.
const float rounding = -1e-3f * max({p[0], p[1], p[2]});
for (size_t c = 0; c < 3; c++) {
if (!isfinite(p[c])) {
nonfinite = true;
p[c] = 0;
} else if (p[c] < 0) {
negative |= p[c] < rounding;
p[c] = 0;
}
}
}
static uint16_t
hdr_encode(float linear)
{
return uint16_t(lround(transfer_encode(linear, Transfer::Srgb) * 65535));
}
bool
split_hdr(Image &image, const OpenContext &ctx, span<float> rgba,
bool premultiplied, const double primaries[6], Error *error)
{
const size_t pixels = size_t(image.width) * image.height;
if (rgba.size() != pixels * 4) {
set_error(error, _("invalid or truncated frame"));
return false;
}
for (size_t i = 0; premultiplied && i < pixels; i++) {
float *p = &rgba[i * 4];
if (p[3] > 0)
for (size_t c = 0; c < 3; c++)
p[c] /= p[3];
}
// What BT.2020 does not take in is clamped below.
primaries = widen_negative(rgba, primaries);
auto profile = cmm_or_default(ctx)->get_profile_parametric(
nullopt, kD65White, primaries);
if (!profile) {
set_error(error, _("failed to describe the colour space"));
return false;
}
bool nonfinite = false, negative = false;
float peak = 0;
for (size_t i = 0; i < pixels; i++) {
float *p = &rgba[i * 4];
sanitize_hdr_pixel(p, nonfinite, negative);
peak = max({peak, p[0], p[1], p[2]});
}
if (nonfinite)
add_warning(ctx, _("Non-finite values have been replaced with zero"));
// What stays negative past BT.2020 no display could show anyway,
// and lossy coding leaves plenty of it as noise around black.
if (negative)
fprintf(stderr, "libdn: %s: negative values clamped to zero\n",
ctx.uri.empty() ? "(data)" : ctx.uri.c_str());
// The alternate follows the input to its peak, or rolls off onto the cap,
// and the base rolls that off onto SDR white.
const bool split = peak > 1;
const double headroom = min(double(peak), kSplitHeadroom);
const HdrRolloff to_alternate(peak, headroom), to_base(headroom, 1);
unique_ptr<GainMap> map;
if (split && ctx.gain_maps) {
map = make_unique<GainMap>();
map->width = image.width;
map->height = image.height;
map->data.resize(pixels);
map->max = map->alternate_headroom = float(log2(headroom));
}
for (uint32_t y = 0; y < image.height; y++) {
uint16_t *out = row_u16(image, y);
for (uint32_t x = 0; x < image.width; x++, out += 4) {
const size_t i = size_t(y) * image.width + x;
const float *p = &rgba[i * 4];
const float m = max({p[0], p[1], p[2]});
double scale = 1, gain = 0;
if (split && m > 0) {
const double alternate = to_alternate.apply(m);
const double base = to_base.apply(alternate);
scale = base / m;
gain = (log2(alternate) - log2(base)) / log2(headroom);
}
out[0] = hdr_encode(float(p[2] * scale));
out[1] = hdr_encode(float(p[1] * scale));
out[2] = hdr_encode(float(p[0] * scale));
out[3] = uint16_t(lround(p[3] * 65535));
if (map)
map->data[i] = uint16_t(lround(clamp(gain, 0., 1.) * 65535));
}
}
image.effective_profile = std::move(profile);
image.profile_assumed = false;
image.gain_map = std::move(map);
return true;
}
const double *
widen_negative(span<float> rgba, const double primaries[6])
{
bool negative = false;
for (size_t i = 0; i < rgba.size() / 4 && !negative; i++) {
const float *p = &rgba[i * 4];
negative = p[3] > 0 && (p[0] < 0 || p[1] < 0 || p[2] < 0);
}
if (!negative)
return primaries;
const RgbMatrix m = primaries_to_primaries(primaries, kRec2020Primaries);
for (size_t i = 0; i < rgba.size() / 4; i++) {
float *p = &rgba[i * 4];
const double r = p[0], g = p[1], b = p[2];
for (size_t c = 0; c < 3; c++)
p[c] = float(m[0][c] * r + m[1][c] * g + m[2][c] * b);
}
return kRec2020Primaries;
}
bool
cicp_hdr(uint8_t primaries_code, uint8_t transfer, double primaries[6])
{
double white[2] = {};
return (transfer == 16 || transfer == 18) &&
cicp_primaries(primaries_code, primaries, white) &&
white[0] == kD65White[0] && white[1] == kD65White[1];
}
// BT.2100 PQ, from signal to linear light with 1.0 at 203 cd/m² (BT.2408).
static double
pq_to_linear(double e)
{
const double m1 = 2610. / 16384, m2 = 2523. / 4096 * 128,
c1 = 3424. / 4096, c2 = 2413. / 4096 * 32,
c3 = 2392. / 4096 * 32;
const double p = pow(clamp(e, 0., 1.), 1 / m2);
return 10000. / 203 * pow(max(p - c1, 0.) / (c2 - c3 * p), 1 / m1);
}
// BT.2100 HLG's inverse OETF, from signal to scene light in [0, 1].
static double
hlg_to_scene(double e)
{
const double a = 0.17883277, b = 1 - 4 * a, c = 0.5 - a * log(4 * a);
e = clamp(e, 0., 1.);
return e <= 0.5 ? e * e / 3 : (exp((e - c) / a) + b) / 12;
}
bool
split_hdr_signal(Image &image, const OpenContext &ctx, uint8_t transfer,
const double primaries[6], double hlg_peak, bool premultiplied,
Error *error)
{
// The OOTF weighs by luminance: BT.2020's weights, after conversion.
static const double kRec2020Y[3] = {0.2627, 0.6780, 0.0593};
const RgbMatrix m = primaries_to_primaries(primaries, kRec2020Primaries);
double weights[3] = {};
for (size_t c = 0; c < 3; c++)
for (size_t r = 0; r < 3; r++)
weights[c] += kRec2020Y[r] * m[c][r];
// BT.2100's system gamma for the display, and where reference white
// lands on it, relative to its peak.
const double gamma = 1.2 + 0.42 * log10(hlg_peak / 1000);
const double white = pow(hlg_to_scene(0.75), gamma);
const size_t pixels = size_t(image.width) * image.height;
vector<float> rgba(pixels * 4);
for (uint32_t y = 0; y < image.height; y++) {
const uint16_t *p = row_u16(image, y);
for (uint32_t x = 0; x < image.width; x++, p += 4) {
float *out = &rgba[(size_t(y) * image.width + x) * 4];
const double a = p[3] / 65535.;
double e[3], luminance = 0;
for (size_t c = 0; c < 3; c++) {
e[c] = p[2 - c] / 65535.;
if (premultiplied && a > 0)
e[c] = min(e[c] / a, 1.);
if (transfer == 18) {
e[c] = hlg_to_scene(e[c]);
luminance += weights[c] * e[c];
} else {
e[c] = pq_to_linear(e[c]);
}
}
double scale = 1;
if (transfer == 18)
scale = luminance > 0 ? pow(luminance, gamma - 1) / white : 0;
for (size_t c = 0; c < 3; c++)
out[c] = float(e[c] * scale);
out[3] = float(a);
}
}
return split_hdr(image, ctx, rgba, false, primaries, error);
}
// --- Loaders -----------------------------------------------------------------
// The order is the default loading order.
+48
View File
@@ -115,6 +115,31 @@ struct ProfileEncoding {
std::array<std::array<float, 3>, kSamples> encode{};
};
/// CIE 1931 xy of R, G, B primaries, as H.273 Table 2 lists them.
inline constexpr double kRec709Primaries[6] = {
0.640, 0.330, 0.300, 0.600, 0.150, 0.060};
inline constexpr double kRec2020Primaries[6] = {
0.708, 0.292, 0.170, 0.797, 0.131, 0.046};
inline constexpr double kP3Primaries[6] = {
0.680, 0.320, 0.265, 0.690, 0.150, 0.060};
/// CIE 1931 xy of the D65 white point, which all of the above share,
/// bar DCI-P3.
inline constexpr double kD65White[2] = {0.3127, 0.3290};
/// Linear RGB matrices, indexed by column as ProfileEncoding::rgb_to_xyz.
using RgbMatrix = std::array<std::array<double, 3>, 3>;
/// The display colourants of a matrix/TRC encoding in D65 XYZ, adapted by
/// the Bradford transform rather than the profile's own `chad`, so that
/// display white lands on D65 when the colourants sum to D50.
RgbMatrix display_colourants_d65(const ProfileEncoding &encoding);
/// From display linear RGB into linear RGB of CIE 1931 xy `primaries`
/// (R, G, B) with a D65 white.
RgbMatrix display_to_primaries(
const ProfileEncoding &encoding, const double primaries[6]);
/// Between linear RGB of two sets of CIE 1931 xy primaries, D65 white both.
RgbMatrix primaries_to_primaries(const double from[6], const double to[6]);
class Cmm;
class Profile
@@ -277,6 +302,21 @@ struct RenderClosure {
const OpenContext &ctx, double scale, Error *error) = 0;
};
/// ISO 21496-1 gain map: one channel, channel-independent metadata.
/// Applied in the display's linear light, after `finish_image()`.
struct GainMap {
std::vector<uint16_t> data; ///< Gamma-encoded normalized log2 gain
uint32_t width = 0;
uint32_t height = 0;
float min = 0, max = 0, gamma = 1;
float offset = 0; ///< Base and alternate alike
float base_headroom = 0; ///< log2
float alternate_headroom = 0; ///< log2
};
/// ISO 21496-1 weight for a display headroom (linear, 1 = SDR).
float gain_map_weight(const GainMap &map, float headroom);
struct Image {
/// Working pixels (see kBytesPerPixel). After successful open/finish:
/// BGRA_PREMUL_4X16LE.
@@ -302,6 +342,8 @@ struct Image {
bool profile_assumed = false;
std::unique_ptr<RenderClosure> render;
/// The HDR rendition, in the stored frame; see OpenContext::gain_maps.
std::unique_ptr<GainMap> gain_map;
ImagePtr page_next;
std::weak_ptr<Image> page_previous;
@@ -374,6 +416,9 @@ struct OpenContext {
int screen_dpi = 96;
bool enhance = false;
bool first_frame_only = false;
/// Decode or synthesize gain maps. Recognized gain maps never become
/// pages, whether this is set or not.
bool gain_maps = false;
/// Loaders to try, by name, in this order; empty means all of them,
/// in the default order. Names this build lacks are skipped.
std::span<const std::string> loaders;
@@ -401,9 +446,12 @@ bool save_exv(const Image &page, std::vector<uint8_t> *out, Error *error);
// --- JPEG --------------------------------------------------------------------
/// Stored dimensions and the grid on which a lossless crop must start.
/// Also counts the Multi-Picture Format images past the primary one,
/// which jpeg_transform() does not keep.
struct JpegGrid {
uint32_t width = 0, height = 0;
uint32_t mcu_width = 0, mcu_height = 0;
uint32_t mpf_images = 0;
};
bool jpeg_grid(std::span<const uint8_t> data, JpegGrid *out, Error *error);
+45 -2
View File
@@ -11,7 +11,10 @@
#include <libdnrs.h>
#include <algorithm>
#include <cstring>
#include <memory>
#include <vector>
using namespace std;
@@ -96,6 +99,37 @@ pack_gray(Image &image, const dnrs_frame &frame, bool alpha, bool wide)
}
}
// EXR and Radiance leave colour to convention: BT.709 primaries, D65 white.
// Negative channels step out of them, as EXR allows, and scRGB means to.
static ImagePtr
load_float_frame(const dnrs_frame &frame, const OpenContext &ctx, Error *error)
{
const bool alpha = frame.format == DNRS_PIXEL_RGBA32F;
const size_t channels = alpha ? 4 : 3;
if (!valid_frame(frame, channels * sizeof(float))) {
set_error(error, _("invalid or truncated frame"));
return nullptr;
}
ImagePtr image = image_new(frame.width, frame.height);
if (!image) {
set_error(error, _("image allocation failure"));
return nullptr;
}
vector<float> rgba(size_t(frame.width) * frame.height * 4, 1);
for (uint32_t y = 0; y < frame.height; y++) {
const uint8_t *src = frame.data + size_t(y) * frame.stride;
float *dst = &rgba[size_t(y) * frame.width * 4];
for (uint32_t x = 0; x < frame.width; x++, dst += 4) {
memcpy(dst, src, channels * sizeof(float));
src += channels * sizeof(float);
}
}
if (!split_hdr(*image, ctx, rgba, false, kRec709Primaries, error))
return nullptr;
return image;
}
static ImagePtr
load_frame(const dnrs_frame &frame, Error *error)
{
@@ -168,6 +202,10 @@ load_frame(const dnrs_frame &frame, Error *error)
pack_rgba16le_to_bgra16(*image,
assume_aligned<const uint16_t>(frame.data), frame.stride, 16);
break;
// load_float_frame() takes these, and the check above turned them away.
case DNRS_PIXEL_RGB32F:
case DNRS_PIXEL_RGBA32F:
break;
}
image->frame_duration =
int64_t(min<uint64_t>(frame.duration_ms, uint64_t(INT64_MAX)));
@@ -251,7 +289,10 @@ load_dnrs(span<const uint8_t> data, const OpenContext &ctx, Error *error)
}
break;
}
ImagePtr frame = load_frame(owned.frame, error);
const bool linear = owned.frame.format == DNRS_PIXEL_RGB32F ||
owned.frame.format == DNRS_PIXEL_RGBA32F;
ImagePtr frame = linear ? load_float_frame(owned.frame, ctx, error)
: load_frame(owned.frame, error);
if (!frame)
return nullptr;
append_frame(frames, frames_tail, std::move(frame));
@@ -275,7 +316,9 @@ load_dnrs(span<const uint8_t> data, const OpenContext &ctx, Error *error)
if (entry.key && entry.value)
frames->text.emplace(entry.key, entry.value);
}
finish_frames(*frames, ctx, nullptr, /*input_premul=*/false);
// split_hdr() has already described what its base is in.
finish_frames(*frames, ctx, frames->effective_profile.get(),
/*input_premul=*/false);
append_page(pages, pages_tail, std::move(frames));
if (ctx.first_frame_only)
break;
+422 -23
View File
@@ -24,10 +24,18 @@
#include <libheif/heif_sequences.h>
#define DAWN_HEIF_SEQUENCES
#endif
// Gain maps need the generic item API, and the transformative properties.
#if LIBHEIF_HAVE_VERSION(1, 18, 0) && defined DAWN_HEIF_PROPERTIES
#include <libheif/heif_items.h>
#define DAWN_HEIF_GAIN_MAPS
#endif
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <memory>
#include <string>
#include <vector>
using namespace std;
@@ -123,13 +131,11 @@ heif_handle_exif(heif_image_handle *handle, const OpenContext &ctx)
return iso_exif_payload(exif);
}
// Decodes a single image handle (either a top-level image, or an auxiliary
// image such as a depth map) into one working-format page, extracting Exif
// and an embedded ICC profile, if present, and bringing it to final working
// premul before returning.
// Decodes a single image handle into working-format pixels as they are coded,
// with no colour management, and with the bitstream's alpha association.
static ImagePtr
load_heif_image(heif_context *hctx, heif_item_id id, heif_image_handle *handle,
const OpenContext &ctx, Error *error)
decode_heif_pixels(
heif_image_handle *handle, bool ignore_transformations, Error *error)
{
int has_alpha = heif_image_handle_has_alpha_channel(handle);
int bit_depth = heif_image_handle_get_luma_bits_per_pixel(handle);
@@ -142,6 +148,7 @@ load_heif_image(heif_context *hctx, heif_item_id id, heif_image_handle *handle,
// (typically 10 or 12). Setting `convert_hdr_to_8bit` is a no-op for
// the interleaved RGB(A) requests below.
heif_decoding_options *opts = heif_decoding_options_alloc();
opts->ignore_transformations = ignore_transformations;
bool use16 = bit_depth > 8;
heif_chroma chroma = heif_chroma_interleaved_RGBA;
if (use16)
@@ -196,8 +203,51 @@ load_heif_image(heif_context *hctx, heif_item_id id, heif_image_handle *handle,
if (!has_alpha)
force_opaque(*result);
}
heif_image_release(image);
return result;
}
// --- True HDR ----------------------------------------------------------------
// PQ (16) or HLG (18) in an nclx box that no ICC profile overrides, with
// primaries of a D65 white, which is what split_hdr() takes. Zero otherwise.
static uint8_t
heif_nclx_hdr_transfer(
const heif_color_profile_nclx *nclx, double primaries[6])
{
const auto transfer = uint8_t(nclx->transfer_characteristics);
return cicp_hdr(uint8_t(nclx->color_primaries), transfer, primaries)
? transfer
: 0;
}
static uint8_t
heif_hdr_transfer(heif_image_handle *handle, double primaries[6])
{
heif_color_profile_nclx *nclx = nullptr;
if (heif_image_handle_get_raw_color_profile_size(handle) ||
heif_image_handle_get_nclx_color_profile(handle, &nclx).code || !nclx)
return 0;
const uint8_t transfer = heif_nclx_hdr_transfer(nclx, primaries);
heif_nclx_color_profile_free(nclx);
return transfer;
}
// Decodes a single image handle (either a top-level image, or an auxiliary
// image such as a depth map) into one working-format page, extracting Exif
// and an embedded ICC profile, if present, and bringing it to final working
// premul before returning.
static ImagePtr
load_heif_image(heif_context *hctx, heif_item_id id, heif_image_handle *handle,
const OpenContext &ctx, Error *error)
{
ImagePtr result = decode_heif_pixels(handle, false, error);
if (!result)
return nullptr;
// TODO(p): Test real behaviour on real transparent images.
int has_alpha = heif_image_handle_has_alpha_channel(handle);
bool bitstream_premul =
has_alpha && heif_image_handle_is_premultiplied_alpha(handle);
@@ -209,8 +259,18 @@ load_heif_image(heif_context *hctx, heif_item_id id, heif_image_handle *handle,
result->orientation = Orientation::Rotate0;
result->exif = heif_handle_exif(handle, ctx);
// split_hdr() leaves its base straight, in the profile it names.
double primaries[6] = {};
if (const uint8_t transfer = heif_hdr_transfer(handle, primaries)) {
// HLG's nominal display, as BT.2408 has it.
if (!split_hdr_signal(*result, ctx, transfer, primaries, 1000,
bitstream_premul, error))
return nullptr;
finish_frames(*result, ctx, result->effective_profile.get(), false);
return result;
}
result->icc = heif_handle_profile(handle, ctx);
heif_image_release(image);
// Bring the page to final working premul: colour-manage against any
// embedded ICC profile (derived automatically from result->icc), first
@@ -220,12 +280,294 @@ load_heif_image(heif_context *hctx, heif_item_id id, heif_image_handle *handle,
return result;
}
// --- Gain maps ---------------------------------------------------------------
#ifdef DAWN_HEIF_GAIN_MAPS
static constexpr const char *kAppleGainMapAux =
"urn:com:apple:photo:2020:aux:hdrgainmap";
namespace
{
// libheif does not know the ISO 21496-1 `tmap` derived item, so it never
// becomes an image, but its payload and references can still be read.
struct HeifToneMap {
heif_item_id base = 0; ///< The SDR rendition, as a rule
heif_item_id map = 0; ///< Possibly a top-level image of its own
vector<uint8_t> metadata;
};
} // namespace
static vector<uint8_t>
heif_item_payload(heif_context *hctx, heif_item_id id)
{
uint8_t *data = nullptr;
size_t size = 0;
vector<uint8_t> result;
if (!heif_item_get_item_data(hctx, id, nullptr, &data, &size).code && data)
result.assign(data, data + size);
if (data)
heif_release_item_data(hctx, &data);
return result;
}
// The items that `from` references with the first reference of `type`,
// in file order.
static vector<heif_item_id>
heif_item_references(heif_context *hctx, heif_item_id from, uint32_t type)
{
vector<heif_item_id> result;
for (int index = 0; result.empty(); index++) {
uint32_t found = 0;
heif_item_id *to = nullptr;
const size_t n =
heif_context_get_item_references(hctx, from, index, &found, &to);
if (n && found == type)
result.assign(to, to + n);
if (to)
heif_release_item_references(hctx, &to);
if (!n)
break;
}
return result;
}
static vector<heif_item_id>
heif_items_of_type(heif_context *hctx, uint32_t type)
{
int n = heif_context_get_number_of_items(hctx);
vector<heif_item_id> ids(size_t(max(n, 0)));
n = heif_context_get_list_of_item_IDs(hctx, ids.data(), n);
ids.resize(size_t(max(n, 0)));
erase_if(ids, [&](heif_item_id id) {
return heif_item_get_item_type(hctx, id) != type;
});
return ids;
}
static vector<HeifToneMap>
heif_tone_maps(heif_context *hctx)
{
vector<HeifToneMap> result;
for (heif_item_id id :
heif_items_of_type(hctx, heif_fourcc('t', 'm', 'a', 'p'))) {
vector<heif_item_id> inputs =
heif_item_references(hctx, id, heif_fourcc('d', 'i', 'm', 'g'));
if (inputs.size() == 2)
result.push_back(
{inputs[0], inputs[1], heif_item_payload(hctx, id)});
}
return result;
}
// XMP describing `id`, from a `mime` item that references it with `cdsc`.
static string
heif_item_xmp(heif_context *hctx, heif_item_id id)
{
for (heif_item_id mime : heif_items_of_type(hctx, heif_item_type_mime)) {
const char *type = heif_item_get_mime_item_content_type(hctx, mime);
if (!type || strcmp(type, "application/rdf+xml"))
continue;
vector<heif_item_id> described =
heif_item_references(hctx, mime, heif_fourcc('c', 'd', 's', 'c'));
if (find(described.begin(), described.end(), id) != described.end()) {
vector<uint8_t> xmp = heif_item_payload(hctx, mime);
return string(xmp.begin(), xmp.end());
}
}
return {};
}
// libheif bakes the base item's own clap, irot and imir into its pixels, in
// property order, so the map gets the same, with the crop scaled to its
// resolution. The map's own properties cannot be trusted to match: Apple
// repeats the base's irot on it, and libavif copies clap over unscaled.
static void
heif_transform_gain_map(heif_context *hctx, heif_item_id base,
heif_image_handle *base_handle, GainMap &map)
{
int n = heif_item_get_transformation_properties(hctx, base, nullptr, 0);
vector<heif_property_id> properties(size_t(max(n, 0)));
n = heif_item_get_transformation_properties(
hctx, base, properties.data(), n);
int width = heif_image_handle_get_ispe_width(base_handle);
int height = heif_image_handle_get_ispe_height(base_handle);
for (int i = 0; i < n; i++) {
const heif_property_id property = properties[size_t(i)];
switch (heif_item_get_property_type(hctx, base, property)) {
case heif_item_property_type_transform_crop: {
int left = 0, top = 0, right = 0, bottom = 0;
heif_item_get_property_transform_crop_borders(hctx, base, property,
width, height, &left, &top, &right, &bottom);
if (width <= 0 || height <= 0)
break;
const double sx = double(map.width) / width,
sy = double(map.height) / height;
const auto x0 = uint32_t(lround(left * sx)),
y0 = uint32_t(lround(top * sy)),
x1 = uint32_t(lround((width - right) * sx)),
y1 = uint32_t(lround((height - bottom) * sy));
if (x1 > x0 && y1 > y0)
crop_gain_map(map, x0, y0, x1 - x0, y1 - y0);
width -= left + right;
height -= top + bottom;
break;
}
case heif_item_property_type_transform_rotation: {
const int ccw = heif_item_get_property_transform_rotation_ccw(
hctx, base, property);
if (ccw < 0)
break;
rotate_gain_map(map, ccw);
if (ccw % 180)
swap(width, height);
break;
}
case heif_item_property_type_transform_mirror:
switch (
heif_item_get_property_transform_mirror(hctx, base, property)) {
case heif_transform_mirror_direction_horizontal:
mirror_gain_map(map, true);
break;
case heif_transform_mirror_direction_vertical:
mirror_gain_map(map, false);
break;
default:
break;
}
break;
default:
break;
}
}
}
static void
attach_heif_gain_map(heif_context *hctx, heif_item_id base,
heif_image_handle *base_handle, heif_image_handle *map_handle,
const GainMap &metadata, bool apple, Image &page, const OpenContext &ctx)
{
// Nothing but the pixels, in the map's stored frame.
Error error;
ImagePtr pixels = decode_heif_pixels(map_handle, true, &error);
if (!pixels) {
add_warning(
ctx, format_message(_("gain map: %s"), error.message.c_str()));
return;
}
page.gain_map = make_gain_map(*pixels, metadata, apple);
if (page.gain_map)
heif_transform_gain_map(hctx, base, base_handle, *page.gain_map);
}
static void
load_heif_tone_map(heif_context *hctx, heif_item_id id,
heif_image_handle *handle, const vector<HeifToneMap> &tone_maps,
Image &page, const OpenContext &ctx)
{
auto tmap = find_if(tone_maps.begin(), tone_maps.end(),
[&](const HeifToneMap &candidate) { return candidate.base == id; });
GainMap metadata;
if (tmap == tone_maps.end() || !ctx.gain_maps ||
!parse_tmap_gain_map(tmap->metadata, &metadata) ||
!gain_map_applies(metadata, ctx))
return;
heif_image_handle *map = nullptr;
heif_error err = heif_context_get_image_handle(hctx, tmap->map, &map);
if (err.code != heif_error_Ok) {
add_warning(ctx, err.message);
return;
}
attach_heif_gain_map(hctx, id, handle, map, metadata, false, page, ctx);
heif_image_handle_release(map);
}
static bool
heif_is_apple_gain_map(const heif_image_handle *aux)
{
const char *type = nullptr;
if (heif_image_handle_get_auxiliary_type(aux, &type).code || !type)
return false;
const bool result = !strcmp(type, kAppleGainMapAux);
heif_image_handle_release_auxiliary_type(aux, &type);
return result;
}
// Apple's pre-ISO map, which only counts where no ISO map has been attached.
static void
load_heif_apple_gain_map(heif_context *hctx, heif_item_id base,
heif_image_handle *base_handle, heif_item_id map_id,
heif_image_handle *map_handle, Image &page, const OpenContext &ctx)
{
if (!ctx.gain_maps || page.gain_map)
return;
const double headroom =
apple_gain_map_headroom(heif_item_xmp(hctx, map_id), page.exif);
if (!(headroom > 0))
return;
const GainMap metadata = apple_gain_map(headroom);
if (gain_map_applies(metadata, ctx))
attach_heif_gain_map(
hctx, base, base_handle, map_handle, metadata, true, page, ctx);
}
#else // ! DAWN_HEIF_GAIN_MAPS
namespace
{
struct HeifToneMap {
heif_item_id base = 0;
heif_item_id map = 0;
};
} // namespace
static vector<HeifToneMap>
heif_tone_maps(heif_context *)
{
return {};
}
static void
load_heif_tone_map(heif_context *, heif_item_id, heif_image_handle *,
const vector<HeifToneMap> &, Image &, const OpenContext &)
{
}
static bool
heif_is_apple_gain_map(const heif_image_handle *)
{
return false;
}
static void
load_heif_apple_gain_map(heif_context *, heif_item_id, heif_image_handle *,
heif_item_id, heif_image_handle *, Image &, const OpenContext &)
{
}
#endif // ! DAWN_HEIF_GAIN_MAPS
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Appends any auxiliary images (e.g. depth maps) hanging off `top`
// as further pages. We have no special processing for them yet,
// so they are included mainly to not lose them silently.
// Apple's gain map is not a page, but goes to `page`, if any.
static void
load_heif_aux_images(const OpenContext &ctx, heif_context *hctx,
heif_image_handle *top, ImagePtr &head, ImagePtr &tail)
heif_item_id top_id, heif_image_handle *top, Image *page, ImagePtr &head,
ImagePtr &tail)
{
// Include the depth image, we have no special processing for it now.
int filter = LIBHEIF_AUX_IMAGE_FILTER_OMIT_ALPHA;
@@ -245,6 +587,13 @@ load_heif_aux_images(const OpenContext &ctx, heif_context *hctx,
add_warning(ctx, err.message);
continue;
}
if (heif_is_apple_gain_map(handle)) {
if (page)
load_heif_apple_gain_map(
hctx, top_id, top, ids[size_t(i)], handle, *page, ctx);
heif_image_handle_release(handle);
continue;
}
Error suberror;
ImagePtr aux =
@@ -268,9 +617,11 @@ load_heif_aux_images(const OpenContext &ctx, heif_context *hctx,
static constexpr uint64_t kSequenceBudget = uint64_t(256) << 20;
// The colour information of the file's primary still item, if it has one:
// libheif attaches none to sequence samples.
// libheif attaches none to sequence samples. PQ and HLG go to `transfer`
// and `primaries` instead, as heif_hdr_transfer() has it.
static vector<uint8_t>
heif_primary_profile(heif_context *hctx, const OpenContext &ctx)
heif_primary_profile(heif_context *hctx, const OpenContext &ctx,
uint8_t *transfer, double primaries[6])
{
heif_item_id id = 0;
heif_image_handle *handle = nullptr;
@@ -278,15 +629,18 @@ heif_primary_profile(heif_context *hctx, const OpenContext &ctx)
heif_context_get_image_handle(hctx, id, &handle).code)
return {};
vector<uint8_t> icc = heif_handle_profile(handle, ctx);
vector<uint8_t> icc;
if (!(*transfer = heif_hdr_transfer(handle, primaries)))
icc = heif_handle_profile(handle, ctx);
heif_image_handle_release(handle);
return icc;
}
// The sample's own colour information, which is not the track's.
// See heif_handle_profile().
// See heif_handle_profile() and heif_primary_profile().
static vector<uint8_t>
heif_sample_profile(const heif_image *img, const OpenContext &ctx)
heif_sample_profile(const heif_image *img, const OpenContext &ctx,
uint8_t *transfer, double primaries[6])
{
vector<uint8_t> icc(heif_image_get_raw_color_profile_size(img));
if (!icc.empty()) {
@@ -301,7 +655,9 @@ heif_sample_profile(const heif_image *img, const OpenContext &ctx)
if (heif_image_get_nclx_color_profile(img, &nclx).code || !nclx)
return {};
vector<uint8_t> result = heif_nclx_profile(nclx, ctx);
vector<uint8_t> result;
if (!(*transfer = heif_nclx_hdr_transfer(nclx, primaries)))
result = heif_nclx_profile(nclx, ctx);
heif_nclx_color_profile_free(nclx);
return result;
}
@@ -388,7 +744,10 @@ load_heif_sequence(heif_context *hctx, const OpenContext &ctx)
// Samples carry no colour information of their own, so the primary still
// item's profile stands in for it, and the first sample's for that.
vector<uint8_t> inherited = heif_primary_profile(hctx, ctx);
uint8_t inherited_transfer = 0;
double inherited_primaries[6] = {};
vector<uint8_t> inherited = heif_primary_profile(
hctx, ctx, &inherited_transfer, inherited_primaries);
shared_ptr<Profile> inherited_profile;
bool inherited_assumed = false;
@@ -415,22 +774,41 @@ load_heif_sequence(heif_context *hctx, const OpenContext &ctx)
// and fits int64_t because libheif reports it as a uint32_t.
frame->frame_duration =
int64_t(uint64_t(heif_image_get_duration(img)) * 1000 / timescale);
frame->icc = heif_sample_profile(img, ctx);
uint8_t transfer = 0;
double primaries[6] = {};
frame->icc = heif_sample_profile(img, ctx, &transfer, primaries);
bool bitstream_premul =
has_alpha && heif_image_is_premultiplied_alpha(img);
heif_image_release(img);
if (!transfer && frame->icc.empty() && inherited_transfer) {
transfer = inherited_transfer;
copy(begin(inherited_primaries), end(inherited_primaries),
primaries);
}
// finish_frames() would force the head's profile onto every frame,
// overriding whatever they embed themselves.
Profile *source = nullptr;
if (frame->icc.empty()) {
if (transfer) {
// Maps are per page, and only still images have them.
OpenContext frame_ctx = ctx;
frame_ctx.gain_maps = false;
Error suberror;
if (!split_hdr_signal(*frame, frame_ctx, transfer, primaries, 1000,
bitstream_premul, &suberror)) {
add_warning(ctx, suberror.message);
break;
}
source = frame->effective_profile.get();
bitstream_premul = false;
} else if (frame->icc.empty()) {
frame->icc = inherited;
frame->effective_profile = inherited_profile;
frame->profile_assumed = inherited_assumed;
source = inherited_profile.get();
}
finish_image(*frame, ctx, source, bitstream_premul);
if (!inherited_profile) {
if (!inherited_profile && !transfer) {
inherited = frame->icc;
inherited_profile = frame->effective_profile;
inherited_assumed = frame->profile_assumed;
@@ -515,6 +893,16 @@ load_heif(span<const uint8_t> data, const OpenContext &ctx, Error *error)
vector<heif_item_id> ids(size_t(max(n, 0)));
n = heif_context_get_list_of_top_level_image_IDs(hctx, ids.data(), n);
// A gain map item that is not hidden is a top-level image, but no page,
// whether it is going to be used or not.
const vector<HeifToneMap> tone_maps = heif_tone_maps(hctx);
ids.resize(size_t(max(n, 0)));
erase_if(ids, [&](heif_item_id id) {
return any_of(tone_maps.begin(), tone_maps.end(),
[&](const HeifToneMap &tmap) { return tmap.map == id; });
});
n = int(ids.size());
ImagePtr head, tail;
for (int i = 0; i < n; i++) {
heif_image_handle *handle = nullptr;
@@ -524,6 +912,7 @@ load_heif(span<const uint8_t> data, const OpenContext &ctx, Error *error)
continue;
}
Image *page = nullptr; // What takes the file's own gain maps
if (sequence && ids[size_t(i)] == primary_id) {
// Track samples come out of libheif exactly as coded: it
// applies no `tkhd` matrix, and exposes neither that matrix nor
@@ -538,17 +927,27 @@ load_heif(span<const uint8_t> data, const OpenContext &ctx, Error *error)
append_page(head, tail, std::move(sequence));
} else {
Error suberror;
ImagePtr page =
ImagePtr image =
load_heif_image(hctx, ids[size_t(i)], handle, ctx, &suberror);
if (page)
append_page(head, tail, std::move(page));
else
if (!image) {
add_warning(ctx, suberror.message);
} else {
// Where the base is true HDR, split_hdr() makes the map,
// and the file's own are ignored.
double primaries[6] = {};
if (!heif_hdr_transfer(handle, primaries)) {
page = image.get();
load_heif_tone_map(
hctx, ids[size_t(i)], handle, tone_maps, *page, ctx);
}
append_page(head, tail, std::move(image));
}
}
// TODO(p): Possibly add thumbnail images as well.
if (!ctx.first_frame_only)
load_heif_aux_images(ctx, hctx, handle, head, tail);
load_heif_aux_images(
ctx, hctx, ids[size_t(i)], handle, page, head, tail);
heif_image_handle_release(handle);
if (ctx.first_frame_only)
+142 -15
View File
@@ -19,7 +19,9 @@
#include <CoreGraphics/CoreGraphics.h>
#include <ImageIO/ImageIO.h>
#include <cmath>
#include <cstring>
#include <memory>
#include <string>
#include <vector>
@@ -120,28 +122,100 @@ imageio_orientation(CFDictionaryRef properties)
return Orientation(int(value));
}
// --- Gain maps ---------------------------------------------------------------
// Only one-component 8-bit maps have been seen, 'L008' as CoreVideo calls it.
static unique_ptr<GainMap>
imageio_gain_map_pixels(CFDictionaryRef info, const GainMap &metadata)
{
auto data =
(CFDataRef) CFDictionaryGetValue(info, kCGImageAuxiliaryDataInfoData);
auto description = (CFDictionaryRef) CFDictionaryGetValue(
info, kCGImageAuxiliaryDataInfoDataDescription);
double width = 0, height = 0, stride = 0, format = 0;
if (!data || !description ||
!imageio_number(description, CFSTR("Width"), &width) ||
!imageio_number(description, CFSTR("Height"), &height) ||
!imageio_number(description, CFSTR("BytesPerRow"), &stride) ||
!imageio_number(description, CFSTR("PixelFormat"), &format) ||
uint32_t(format) != fourcc('L', '0', '0', '8') || width < 1 ||
height < 1 || width > kMaxDimension || height > kMaxDimension ||
stride < width ||
double(CFDataGetLength(data)) < stride * (height - 1) + width)
return nullptr;
ImagePtr pixels = image_new(uint32_t(width), uint32_t(height));
if (!pixels)
return nullptr;
const uint8_t *src = CFDataGetBytePtr(data);
for (uint32_t y = 0; y < pixels->height; y++) {
uint16_t *d = row_u16(*pixels, y);
const uint8_t *s = src + size_t(y) * size_t(stride);
for (uint32_t x = 0; x < pixels->width; x++, d += 4)
d[0] = d[1] = d[2] = d[3] = uint16_t(s[x] * 257);
}
return make_gain_map(*pixels, metadata, true);
}
// Apple's pre-ISO map, as its cameras put in HEIC and JPEG. From macOS 15 on,
// ImageIO also reports ISO 21496-1 maps, but describes their metadata its own
// way, which nothing here has been checked against, so those are left alone.
API_AVAILABLE(macos(11.0))
static void
imageio_gain_map(CGImageSourceRef source, size_t index,
CFDictionaryRef properties, Image &image, const OpenContext &ctx)
{
CFDictionaryRef info = CGImageSourceCopyAuxiliaryDataInfoAtIndex(
source, index, kCGImageAuxiliaryDataTypeHDRGainMap);
if (!info)
return;
string xmp;
auto metadata = (CGImageMetadataRef) CFDictionaryGetValue(
info, kCGImageAuxiliaryDataInfoMetadata);
CFDataRef packet =
metadata ? CGImageMetadataCreateXMPData(metadata, nullptr) : nullptr;
if (packet) {
xmp.assign((const char *) CFDataGetBytePtr(packet),
size_t(CFDataGetLength(packet)));
CFRelease(packet);
}
// ImageIO hands out no Exif, but it does parse Apple's maker note.
double maker33 = NAN, maker48 = 0;
CFTypeRef maker =
CFDictionaryGetValue(properties, kCGImagePropertyMakerAppleDictionary);
if (maker && CFGetTypeID(maker) == CFDictionaryGetTypeID()) {
(void) imageio_number((CFDictionaryRef) maker, CFSTR("33"), &maker33);
(void) imageio_number((CFDictionaryRef) maker, CFSTR("48"), &maker48);
}
const double headroom =
apple_gain_map_headroom_from_maker(xmp, maker33, maker48);
if (headroom > 0) {
const GainMap map = apple_gain_map(headroom);
if (gain_map_applies(map, ctx))
image.gain_map = imageio_gain_map_pixels(info, map);
}
CFRelease(info);
}
// --- Pixels ------------------------------------------------------------------
// Core Graphics converts from the image's colour space to the context's,
// so giving the context the source's own space makes drawing an identity,
// and leaves all colour management to lcms2, as with every other loader.
// Anything that cannot be one--Gray, CMYK, Indexed, Lab, extended range--gets
// a real conversion to sRGB instead, which is then no longer an assumption.
// Anything that cannot be one--Gray, CMYK, Indexed, Lab--gets a real
// conversion to sRGB instead, which is then no longer an assumption.
static CGColorSpaceRef
imageio_target_space(
CGImageRef cg, const OpenContext &ctx, CFDataRef *icc, Error *error)
imageio_target_space(CGImageRef cg, CFDataRef *icc, Error *error)
{
CGColorSpaceRef source = CGImageGetColorSpace(cg);
if (source && CGColorSpaceGetModel(source) == kCGColorSpaceModelRGB &&
!CGColorSpaceUsesExtendedRange(source) &&
(*icc = CGColorSpaceCopyICCData(source)))
return CGColorSpaceRetain(source);
// Display P3 would narrow the loss without removing it, and this is
// a fallback loader--revisit if anyone actually views EXR files in anger.
if (source && CGColorSpaceUsesExtendedRange(source))
add_warning(ctx, _("extended range colours clipped to sRGB"));
CGColorSpaceRef srgb = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
if (!srgb) {
set_error(error, _("cannot create an sRGB colour space"));
@@ -151,6 +225,44 @@ imageio_target_space(
return srgb;
}
// Extended range, as EXR, Radiance and float TIFF come, is linear light with
// 1.0 at SDR white. In BT.2020, little of scRGB stays negative.
static ImagePtr
load_imageio_hdr(CGImageRef cg, const OpenContext &ctx, Error *error)
{
size_t width = CGImageGetWidth(cg), height = CGImageGetHeight(cg);
CGColorSpaceRef space =
CGColorSpaceCreateWithName(kCGColorSpaceExtendedLinearITUR_2020);
if (!space) {
set_error(error, _("failed to describe the colour space"));
return nullptr;
}
vector<float> rgba(width * height * 4);
CGContextRef context = CGBitmapContextCreate(rgba.data(), width, height, 32,
width * 4 * sizeof(float), space,
CGBitmapInfo(kCGImageAlphaPremultipliedLast) |
kCGBitmapFloatComponents | kCGBitmapByteOrder32Little);
CGColorSpaceRelease(space);
if (!context) {
set_error(error, _("cannot create a bitmap context"));
return nullptr;
}
CGContextSetBlendMode(context, kCGBlendModeCopy);
CGContextDrawImage(
context, CGRectMake(0, 0, double(width), double(height)), cg);
CGContextRelease(context);
ImagePtr image = image_new(uint32_t(width), uint32_t(height));
if (!image) {
set_error(error, _("image allocation failure"));
return nullptr;
}
if (!split_hdr(*image, ctx, rgba, true, kRec2020Primaries, error))
return nullptr;
return image;
}
static ImagePtr
load_imageio_image(CGImageRef cg, const OpenContext &ctx, Error *error)
{
@@ -160,8 +272,12 @@ load_imageio_image(CGImageRef cg, const OpenContext &ctx, Error *error)
return nullptr;
}
CGColorSpaceRef source = CGImageGetColorSpace(cg);
if (source && CGColorSpaceUsesExtendedRange(source))
return load_imageio_hdr(cg, ctx, error);
CFDataRef icc = nullptr;
CGColorSpaceRef space = imageio_target_space(cg, ctx, &icc, error);
CGColorSpaceRef space = imageio_target_space(cg, &icc, error);
if (!space)
return nullptr;
@@ -240,10 +356,13 @@ load_imageio_indexes(CGImageSourceRef source, CFDictionaryRef options,
Error suberror;
ImagePtr image;
bool hdr = false;
CGImageRef cg = CGImageSourceCreateImageAtIndex(source, i, options);
if (!cg) {
set_error(&suberror, _("ImageIO decoding error"));
} else {
CGColorSpaceRef space = CGImageGetColorSpace(cg);
hdr = space && CGColorSpaceUsesExtendedRange(space);
image = load_imageio_image(cg, ctx, &suberror);
CGImageRelease(cg);
}
@@ -252,6 +371,11 @@ load_imageio_indexes(CGImageSourceRef source, CFDictionaryRef options,
if (!CFDictionaryContainsKey(
properties, kCGImagePropertyProfileName))
imageio_drop_invented_srgb(*image);
// A true HDR base ignores the file's map, as in HEIF.
if (@available(macOS 11, *)) {
if (!animated && !hdr && ctx.gain_maps)
imageio_gain_map(source, i, properties, *image, ctx);
}
}
if (properties)
CFRelease(properties);
@@ -282,9 +406,11 @@ load_imageio_indexes(CGImageSourceRef source, CFDictionaryRef options,
return nullptr;
}
// Only split_hdr() names a profile, for a base it leaves straight.
head->loops = loops;
for (Image *page = head.get(); page; page = page->page_next.get())
finish_frames(*page, ctx, nullptr, /*input_premul=*/true);
finish_frames(*page, ctx, page->effective_profile.get(),
/*input_premul=*/!page->effective_profile);
return head;
}
@@ -300,10 +426,11 @@ load_imageio(span<const uint8_t> data, const OpenContext &ctx, Error *error)
return nullptr;
}
const void *keys[] = {kCGImageSourceShouldCache};
const void *values[] = {kCFBooleanFalse};
const void *keys[] = {
kCGImageSourceShouldCache, kCGImageSourceShouldAllowFloat};
const void *values[] = {kCFBooleanFalse, kCFBooleanTrue};
CFDictionaryRef options =
CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1,
CFDictionaryCreate(kCFAllocatorDefault, keys, values, 2,
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CGImageSourceRef source = CGImageSourceCreateWithData(wrapper, options);
+128 -54
View File
@@ -43,12 +43,20 @@ namespace dawn
// --- Multi-Picture Format ----------------------------------------------------
static uint32_t
parse_mpf_mpentry(const uint8_t *p, const tiffer *T)
namespace
{
uint32_t attrs = T->un->u32(p);
uint32_t offset = T->un->u32(p + 8);
struct MpfEntry {
const uint8_t *jpeg; ///< Where the individual image starts
bool page; ///< Whether it is a kind of image to show
};
} // namespace
// Whether an individual image is of a kind to show on its own.
static bool
mpf_is_page(uint32_t attrs)
{
enum {
TypeBaselineMPPrimaryImage = 0x030000,
TypeLargeThumbnailVGA = 0x010001,
@@ -63,62 +71,52 @@ parse_mpf_mpentry(const uint8_t *p, const tiffer *T)
case TypeLargeThumbnailFullHD:
// Wasted cycles.
case TypeUndefined:
// Apple uses this for HDR and depth maps (same and lower resolution).
// TODO(p): It would be nice to be able to view them.
return 0;
// Apple uses this for HDR and depth maps (same and lower resolution),
// Ultra HDR for its gain map.
// TODO(p): It would be nice to be able to view depth maps.
return false;
}
// Don't report non-JPEGs, even though they're unlikely.
if (((attrs >> 24) & 0x7) != 0)
return 0;
return offset;
return ((attrs >> 24) & 0x7) == 0;
}
static vector<uint32_t>
parse_mpf_index_entries(const tiffer *T, const tiffer_entry *entry)
static void
parse_mpf_index_entries(vector<MpfEntry> &individuals, const tiffer *T,
const tiffer_entry *entry, const uint8_t *mpf, size_t total_len)
{
uint32_t count = entry->remaining_count / 16;
vector<uint32_t> offsets;
offsets.reserve(count);
for (uint32_t i = 0; i < count; i++) {
// 5.2.3.3.3. Individual Image Data Offset
uint32_t offset = parse_mpf_mpentry(entry->p + i * 16, T);
if (offset)
offsets.push_back(offset);
// 5.2.3.3.3. Individual Image Data Offset,
// which is zero for the primary image.
const uint8_t *p = entry->p + i * 16;
uint32_t offset = T->un->u32(p + 8);
if (offset && offset <= total_len)
individuals.push_back({mpf + offset, mpf_is_page(T->un->u32(p))});
}
return offsets;
}
static vector<uint32_t>
parse_mpf_index_ifd(tiffer *T)
{
tiffer_entry entry = {};
while (tiffer_next_entry(T, &entry)) {
// 5.2.3.3. MP Entry
if (entry.tag == MPF_MPEntry && entry.type == TIFFER_UNDEFINED &&
!(entry.remaining_count % 16)) {
return parse_mpf_index_entries(T, &entry);
}
}
return {};
}
/// Collects pointers (into `mpf`) to the individual JPEGs of an MPF.
static bool
parse_mpf(vector<const uint8_t *> &individuals, const uint8_t *mpf, size_t len,
/// Collects pointers (into `mpf`) to the individual JPEGs of an MPF,
/// all but the primary one.
static void
parse_mpf(vector<MpfEntry> &individuals, const uint8_t *mpf, size_t len,
size_t total_len)
{
tiffer T = {};
if (!tiffer_init(&T, mpf, len) || !tiffer_next_ifd(&T))
return false;
return;
// First image: IFD0 is Index IFD, any IFD1 is Attribute IFD.
// Other images: IFD0 is Attribute IFD, there is no Index IFD.
for (uint32_t offset : parse_mpf_index_ifd(&T))
if (offset && offset <= total_len)
individuals.push_back(mpf + offset);
return true;
tiffer_entry entry = {};
while (tiffer_next_entry(&T, &entry)) {
// 5.2.3.3. MP Entry
if (entry.tag == MPF_MPEntry && entry.type == TIFFER_UNDEFINED &&
!(entry.remaining_count % 16)) {
parse_mpf_index_entries(individuals, &T, &entry, mpf, total_len);
return;
}
}
}
// --- Exif-derived colour profile ---------------------------------------------
@@ -222,9 +220,11 @@ namespace
{
struct JpegMetadata {
vector<uint8_t> exif; ///< Exif buffer, may be empty
vector<uint8_t> icc; ///< ICC profile buffer, may be empty
vector<const uint8_t *> mpf; ///< Multi-Picture Format entries
vector<uint8_t> exif; ///< Exif buffer, may be empty
vector<uint8_t> icc; ///< ICC profile buffer, may be empty
string xmp; ///< The main XMP packet, may be empty
vector<uint8_t> iso; ///< ISO 21496-1 metadata, may be empty
vector<MpfEntry> mpf; ///< Multi-Picture Format entries
};
} // namespace
@@ -336,7 +336,18 @@ parse_jpeg_metadata(span<const uint8_t> data, JpegMetadata *meta)
meta->mpf, payload, size_t(p - payload), size_t(end - payload));
}
// TODO(p): Extract the main XMP segment.
// Adobe XMP Specification Part 3: Storage in Files, 2020/1, 1.1.3
static constexpr char xmp_ns[] = "http://ns.adobe.com/xap/1.0/";
if (marker == APP1 && size_t(p - payload) >= sizeof xmp_ns &&
!memcmp(payload, xmp_ns, sizeof xmp_ns) && meta->xmp.empty())
meta->xmp.assign(
(const char *) payload + sizeof xmp_ns, (const char *) p);
// ISO 21496-1 metadata, as Ultra HDR 1.1 stores it in JPEG.
static constexpr char iso_ns[] = "urn:iso:std:iso:ts:21496:-1";
if (marker == APP2 && size_t(p - payload) >= sizeof iso_ns &&
!memcmp(payload, iso_ns, sizeof iso_ns) && meta->iso.empty())
meta->iso.assign(payload + sizeof iso_ns, p);
}
if (!icc_done)
@@ -505,6 +516,57 @@ pack_jpeg_ext_to_bgra16(
}
}
// --- Gain maps ---------------------------------------------------------------
static constexpr string_view kHdrgmNs = "http://ns.adobe.com/hdr-gain-map/1.0/";
static constexpr string_view kContainerItemNs =
"http://ns.google.com/photos/1.0/container/item/";
/// Recognizes an individual MPF image as a gain map, by its ISO 21496-1
/// metadata, Ultra HDR XMP, or Apple's, and attaches it to the primary `image`
/// when asked to and supported. Returns whether it was a gain map.
static bool
load_jpeg_gain_map(Image &image, span<const uint8_t> jpeg, bool directory_map,
span<const uint8_t> primary_exif, int number, const OpenContext &ctx)
{
JpegMetadata meta;
parse_jpeg_metadata(jpeg, &meta);
GainMap metadata;
bool supported = false, apple = false;
if (!meta.iso.empty()) {
supported = parse_iso_gain_map(meta.iso, &metadata);
} else if (xmp_declares(meta.xmp, kHdrgmNs) || directory_map) {
supported = parse_hdrgm_gain_map(meta.xmp, &metadata);
} else if (apple_gain_map_declared(meta.xmp)) {
apple = true;
const double headroom = apple_gain_map_headroom(meta.xmp, primary_exif);
if ((supported = headroom != 0))
metadata = apple_gain_map(headroom);
} else {
return false;
}
if (!ctx.gain_maps || image.gain_map || !supported ||
!gain_map_applies(metadata, ctx))
return true;
// Nothing but the pixels: no conversion, no smoothing, no recursion.
OpenContext map_ctx;
map_ctx.cmm = ctx.cmm;
map_ctx.first_frame_only = true;
Error error;
ImagePtr pixels = open_libjpeg_turbo(jpeg, map_ctx, &error);
if (pixels)
image.gain_map = make_gain_map(*pixels, metadata, apple);
else
add_warning(ctx,
format_message(
_("MPF image %d: %s"), number, error.message.c_str()));
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/// Finishes a decoded JPEG page: metadata, optional MPF follow-ups, then
/// colour-manage. `bits` is 8 for JSAMPLE output, or 12/16 for high precision.
/// When bits==8, `pixels8` is BGRA8 or CMYK8; otherwise `pixels16` is used.
@@ -516,18 +578,28 @@ load_jpeg_finalize(ImagePtr &image, bool cmyk, int bits, const OpenContext &ctx,
parse_jpeg_metadata(data, &meta);
if (!ctx.first_frame_only) {
// Ultra HDR's container directory lists the primary image first.
vector<string> semantics =
xmp_values(meta.xmp, kContainerItemNs, "Semantic");
// XXX: This is ugly, as it relies on just the first individual image
// having any follow-up entries (as it should be).
ImagePtr tail = image;
for (size_t i = 0; i < meta.mpf.size(); i++) {
const uint8_t *jpeg = meta.mpf[i];
size_t sub_len = size_t((data.data() + data.size()) - jpeg);
const uint8_t *jpeg = meta.mpf[i].jpeg;
span<const uint8_t> sub(
jpeg, size_t((data.data() + data.size()) - jpeg));
const bool directory_map =
i + 1 < semantics.size() && semantics[i + 1] == "GainMap";
if (load_jpeg_gain_map(
*image, sub, directory_map, meta.exif, int(i + 2), ctx) ||
!meta.mpf[i].page)
continue;
Error suberror;
ImagePtr sub = open_libjpeg_turbo(
span<const uint8_t>(jpeg, sub_len), ctx, &suberror);
if (sub)
append_page(image, tail, std::move(sub));
ImagePtr subimage = open_libjpeg_turbo(sub, ctx, &suberror);
if (subimage)
append_page(image, tail, std::move(subimage));
else
add_warning(ctx,
format_message(_("MPF image %d: %s"), int(i + 2),
@@ -813,10 +885,12 @@ jpeg_grid(span<const uint8_t> data, JpegGrid *out, Error *error)
*error = {
Error::Code::Open, _("Unsupported chroma subsampling")};
} else if (out) {
JpegMetadata meta;
parse_jpeg_metadata(data, &meta);
*out = {uint32_t(tj3Get(handle, TJPARAM_JPEGWIDTH)),
uint32_t(tj3Get(handle, TJPARAM_JPEGHEIGHT)),
uint32_t(tjMCUWidth[sampling]),
uint32_t(tjMCUHeight[sampling])};
uint32_t(tjMCUWidth[sampling]), uint32_t(tjMCUHeight[sampling]),
uint32_t(meta.mpf.size())};
}
}
if (handle)
+133 -10
View File
@@ -14,6 +14,7 @@
#if DAWN_WITH_LIBJXL
#include <jxl/decode.h>
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <vector>
@@ -31,8 +32,10 @@ constexpr size_t kBoxChunk = 4096;
// libjxl gives us tightly packed interleaved RGBA; the pack helper reorders to
// the BGRA working format. Requesting float output instead would only be
// truncated back to 16 bits on the way in.
// truncated back to 16 bits on the way in, unless it goes to split_hdr().
constexpr JxlPixelFormat kFormat = {4, JXL_TYPE_UINT16, JXL_LITTLE_ENDIAN, 0};
constexpr JxlPixelFormat kFloatFormat = {
4, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, 0};
namespace
{
@@ -41,13 +44,16 @@ struct JxlLoadContext {
JxlDecoder *dec = nullptr; ///< libjxl decoder
JxlBasicInfo info = {}; ///< Codestream header
vector<uint8_t> icc; ///< ICC profile the output pixels are in
vector<uint8_t> scratch; ///< Interleaved RGBA16 buffer for one frame
uint8_t transfer = 0; ///< H.273 transfer, when true HDR
double primaries[6] = {}; ///< CIE 1931 xy of a true HDR encoding
vector<uint8_t> scratch; ///< Interleaved RGBA buffer for one frame
int64_t duration_ms = 0; ///< Duration of the frame being decoded
vector<uint8_t> box; ///< Payload of the box in progress
vector<uint8_t> *box_dest = nullptr; ///< Where `box` lands, if wanted
vector<uint8_t> meta_exif; ///< Exif, if any was found
vector<uint8_t> meta_xmp; ///< XMP, if any was found
vector<uint8_t> meta_jhgm; ///< Gain map bundle, if any
const OpenContext *octx = nullptr; ///< Caller-supplied context
@@ -110,7 +116,7 @@ finish_box(JxlLoadContext &ctx)
ctx.box_dest = nullptr;
}
// Starts collecting a box, if it is one of the two we care about. The rest
// Starts collecting a box, if it is one of those we care about. The rest
// are left alone for libjxl to skip over.
static void
start_box(JxlLoadContext &ctx)
@@ -124,6 +130,8 @@ start_box(JxlLoadContext &ctx)
ctx.box_dest = &ctx.meta_exif;
else if (!memcmp(type, "xml ", sizeof type))
ctx.box_dest = &ctx.meta_xmp;
else if (!memcmp(type, "jhgm", sizeof type))
ctx.box_dest = &ctx.meta_jhgm;
else
return;
@@ -162,6 +170,47 @@ take_icc_profile(JxlLoadContext &ctx)
}
}
// PQ and HLG go to split_hdr(), and so do float codestreams in linear light,
// like EXR, whatever their peak. All of them need to be in primaries of a D65
// white, and described by an encoding, not by an ICC profile.
static void
take_hdr_encoding(JxlLoadContext &ctx)
{
JxlColorEncoding e = {};
if (JxlDecoderGetColorAsEncodedProfile(
ctx.dec, JXL_COLOR_PROFILE_TARGET_DATA, &e) != JXL_DEC_SUCCESS ||
e.white_point != JXL_WHITE_POINT_D65 ||
(e.color_space != JXL_COLOR_SPACE_RGB &&
e.color_space != JXL_COLOR_SPACE_GRAY))
return;
// Any primaries of a D65 white leave grey alone.
const double *primaries = nullptr;
const double custom[6] = {e.primaries_red_xy[0], e.primaries_red_xy[1],
e.primaries_green_xy[0], e.primaries_green_xy[1],
e.primaries_blue_xy[0], e.primaries_blue_xy[1]};
if (e.color_space == JXL_COLOR_SPACE_GRAY ||
e.primaries == JXL_PRIMARIES_SRGB)
primaries = kRec709Primaries;
else if (e.primaries == JXL_PRIMARIES_2100)
primaries = kRec2020Primaries;
else if (e.primaries == JXL_PRIMARIES_P3)
primaries = kP3Primaries;
else if (e.primaries == JXL_PRIMARIES_CUSTOM)
primaries = custom;
else
return;
if (e.transfer_function == JXL_TRANSFER_FUNCTION_PQ)
ctx.transfer = 16;
else if (e.transfer_function == JXL_TRANSFER_FUNCTION_HLG)
ctx.transfer = 18;
else if (e.transfer_function == JXL_TRANSFER_FUNCTION_LINEAR &&
ctx.info.exponent_bits_per_sample)
ctx.transfer = 8;
copy(primaries, primaries + 6, ctx.primaries);
}
// Frame durations count ticks, whose length the codestream header defines as
// a fraction of a second.
static bool
@@ -187,15 +236,16 @@ take_frame_header(JxlLoadContext &ctx, Error *error)
static bool
bind_frame_buffer(JxlLoadContext &ctx, Error *error)
{
const JxlPixelFormat *format = ctx.transfer == 8 ? &kFloatFormat : &kFormat;
size_t size = 0;
if (JxlDecoderImageOutBufferSize(ctx.dec, &kFormat, &size) !=
if (JxlDecoderImageOutBufferSize(ctx.dec, format, &size) !=
JXL_DEC_SUCCESS) {
set_error(error, _("failed to size the output buffer"));
return false;
}
ctx.scratch.resize(size);
if (JxlDecoderSetImageOutBuffer(ctx.dec, &kFormat, ctx.scratch.data(),
if (JxlDecoderSetImageOutBuffer(ctx.dec, format, ctx.scratch.data(),
ctx.scratch.size()) != JXL_DEC_SUCCESS) {
set_error(error, _("failed to set the output buffer"));
return false;
@@ -203,6 +253,31 @@ bind_frame_buffer(JxlLoadContext &ctx, Error *error)
return true;
}
// Linear light, 1.0 at SDR white by convention: a linear header's
// intensity_target is a loose upper bound, which encoders fill in by default.
static bool
split_float_frame(
JxlLoadContext &ctx, const OpenContext &octx, Image &image, Error *error)
{
span<float> rgba(assume_aligned<float>(ctx.scratch.data()),
ctx.scratch.size() / sizeof(float));
return split_hdr(image, octx, rgba,
ctx.info.alpha_bits && ctx.info.alpha_premultiplied, ctx.primaries,
error);
}
// PQ and HLG come as the signal, which split_hdr_signal() takes in 16 bits
// without loss that matters. HLG's display peak is intensity_target.
static bool
split_signal_frame(
JxlLoadContext &ctx, const OpenContext &octx, Image &image, Error *error)
{
const double peak =
ctx.info.intensity_target > 0 ? ctx.info.intensity_target : 1000;
return split_hdr_signal(image, octx, ctx.transfer, ctx.primaries, peak,
ctx.info.alpha_bits && ctx.info.alpha_premultiplied, error);
}
static bool
append_decoded_frame(JxlLoadContext &ctx, Error *error)
{
@@ -219,9 +294,10 @@ append_decoded_frame(JxlLoadContext &ctx, Error *error)
}
// Coalescing stays on, so every frame covers the whole canvas.
pack_rgba16le_to_bgra16(*image,
assume_aligned<const uint16_t>(ctx.scratch.data()),
size_t(ctx.info.xsize) * 4 * sizeof(uint16_t), 16);
if (ctx.transfer != 8)
pack_rgba16le_to_bgra16(*image,
assume_aligned<const uint16_t>(ctx.scratch.data()),
size_t(ctx.info.xsize) * 4 * sizeof(uint16_t), 16);
image->icc = ctx.icc;
image->orientation = Orientation(ctx.info.orientation);
@@ -229,8 +305,20 @@ append_decoded_frame(JxlLoadContext &ctx, Error *error)
if (ctx.info.have_animation)
image->loops = ctx.info.animation.num_loops;
finish_frames(*image, *ctx.octx, nullptr,
ctx.info.alpha_bits && ctx.info.alpha_premultiplied);
// split_hdr() leaves its base straight, in the profile it names.
// Maps are per page, and only still images have them.
if (ctx.transfer) {
OpenContext octx = *ctx.octx;
octx.gain_maps = octx.gain_maps && !ctx.info.have_animation;
if (!(ctx.transfer == 8 ? split_float_frame(ctx, octx, *image, error)
: split_signal_frame(ctx, octx, *image, error)))
return false;
finish_frames(
*image, *ctx.octx, image->effective_profile.get(), false);
} else {
finish_frames(*image, *ctx.octx, nullptr,
ctx.info.alpha_bits && ctx.info.alpha_premultiplied);
}
append_frame(ctx.result, ctx.result_tail, std::move(image));
return true;
}
@@ -255,6 +343,7 @@ process_event(JxlLoadContext &ctx, bool *done, Error *error)
break;
case JXL_DEC_COLOR_ENCODING:
take_icc_profile(ctx);
take_hdr_encoding(ctx);
break;
case JXL_DEC_FRAME:
return take_frame_header(ctx, error);
@@ -283,6 +372,39 @@ process_event(JxlLoadContext &ctx, bool *done, Error *error)
return true;
}
// --- Gain maps ---------------------------------------------------------------
// Only an SDR base takes a map. A true HDR one went through split_hdr(),
// and any other HDR one is left alone, with a warning.
static void
attach_jxl_gain_map(JxlLoadContext &ctx)
{
const OpenContext &octx = *ctx.octx;
if (!octx.gain_maps || ctx.meta_jhgm.empty() || !ctx.result ||
ctx.info.have_animation || ctx.transfer)
return;
span<const uint8_t> blob, codestream;
GainMap metadata;
if (!split_jhgm_bundle(ctx.meta_jhgm, &blob, &codestream) ||
!parse_iso_gain_map(blob, &metadata) ||
!gain_map_applies(metadata, octx))
return;
// The map is a naked codestream of its own. Nothing but the pixels:
// no conversion, no recursion.
OpenContext map_ctx;
map_ctx.cmm = octx.cmm;
map_ctx.first_frame_only = true;
Error error;
ImagePtr pixels = load_jxl(codestream, map_ctx, &error);
if (pixels)
ctx.result->gain_map = make_gain_map(*pixels, metadata, false);
else
add_warning(
octx, format_message(_("gain map: %s"), error.message.c_str()));
}
// --- Public entry point ------------------------------------------------------
ImagePtr
@@ -303,6 +425,7 @@ load_jxl(span<const uint8_t> data, const OpenContext &octx, Error *error)
return nullptr;
finish_box(ctx);
attach_jxl_gain_map(ctx);
if (!ctx.result) {
set_error(error, _("empty or unsupported image"));
return nullptr;
+154 -27
View File
@@ -21,6 +21,8 @@
#undef max
#include <algorithm>
#include <bit>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <iterator>
@@ -36,12 +38,25 @@ namespace dawn
namespace
{
// How samples are stored. Past the unsigned ones comes the float family:
// linear light in scRGB's primaries, 1.0 at SDR white, and allowed to leave
// both behind.
enum class Sample : uint8_t {
Unorm8,
Unorm16,
Half, ///< IEEE 754 binary16
Float, ///< IEEE 754 binary32
Fixed16, ///< Signed, with 13 fractional bits
Fixed32, ///< Signed, with 24 fractional bits
Rgbe, ///< Bytes sharing an exponent in the fourth one, as Radiance's
};
// How one of jxrlib's pixel formats sits in memory. Every format we read as
// it comes is a run of unsigned samples in memory order, so the channel
// indices and the sample width say all there is to say about it.
// it comes is a run of samples in memory order, so the channel indices and
// the sample type say all there is to say about it.
struct Layout {
uint8_t samples; ///< Samples per pixel, padding included
uint8_t bits; ///< Bits per sample, 8 or 16
Sample type; ///< How each of them is stored
int8_t r, g, b; ///< Colour sample indices
int8_t a; ///< Alpha sample index, negative when opaque
bool premultiplied; ///< Whether that alpha is associated
@@ -57,28 +72,55 @@ struct NativeFormat {
// jxrlib's format converter refuses grey and premultiplied inputs outright,
// so reading these as they come is not an optimisation--it is the only way
// to load a good half of the files that exist.
// The float family has to be read as it comes too, as jxrlib has no float
// target to convert it into.
static const NativeFormat kNativeFormats[] = {
{&GUID_PKPixelFormat8bppGray, {1, 8, 0, 0, 0, -1, false}},
{&GUID_PKPixelFormat16bppGray, {1, 16, 0, 0, 0, -1, false}},
{&GUID_PKPixelFormat24bppRGB, {3, 8, 0, 1, 2, -1, false}},
{&GUID_PKPixelFormat24bppBGR, {3, 8, 2, 1, 0, -1, false}},
{&GUID_PKPixelFormat32bppRGB, {4, 8, 0, 1, 2, -1, false}},
{&GUID_PKPixelFormat32bppBGR, {4, 8, 2, 1, 0, -1, false}},
{&GUID_PKPixelFormat32bppRGBA, {4, 8, 0, 1, 2, 3, false}},
{&GUID_PKPixelFormat32bppBGRA, {4, 8, 2, 1, 0, 3, false}},
{&GUID_PKPixelFormat32bppPRGBA, {4, 8, 0, 1, 2, 3, true}},
{&GUID_PKPixelFormat32bppPBGRA, {4, 8, 2, 1, 0, 3, true}},
{&GUID_PKPixelFormat48bppRGB, {3, 16, 0, 1, 2, -1, false}},
{&GUID_PKPixelFormat64bppRGBA, {4, 16, 0, 1, 2, 3, false}},
{&GUID_PKPixelFormat64bppPRGBA, {4, 16, 0, 1, 2, 3, true}},
{&GUID_PKPixelFormat8bppGray, {1, Sample::Unorm8, 0, 0, 0, -1, false}},
{&GUID_PKPixelFormat16bppGray, {1, Sample::Unorm16, 0, 0, 0, -1, false}},
{&GUID_PKPixelFormat24bppRGB, {3, Sample::Unorm8, 0, 1, 2, -1, false}},
{&GUID_PKPixelFormat24bppBGR, {3, Sample::Unorm8, 2, 1, 0, -1, false}},
{&GUID_PKPixelFormat32bppRGB, {4, Sample::Unorm8, 0, 1, 2, -1, false}},
{&GUID_PKPixelFormat32bppBGR, {4, Sample::Unorm8, 2, 1, 0, -1, false}},
{&GUID_PKPixelFormat32bppRGBA, {4, Sample::Unorm8, 0, 1, 2, 3, false}},
{&GUID_PKPixelFormat32bppBGRA, {4, Sample::Unorm8, 2, 1, 0, 3, false}},
{&GUID_PKPixelFormat32bppPRGBA, {4, Sample::Unorm8, 0, 1, 2, 3, true}},
{&GUID_PKPixelFormat32bppPBGRA, {4, Sample::Unorm8, 2, 1, 0, 3, true}},
{&GUID_PKPixelFormat48bppRGB, {3, Sample::Unorm16, 0, 1, 2, -1, false}},
{&GUID_PKPixelFormat64bppRGBA, {4, Sample::Unorm16, 0, 1, 2, 3, false}},
{&GUID_PKPixelFormat64bppPRGBA, {4, Sample::Unorm16, 0, 1, 2, 3, true}},
{&GUID_PKPixelFormat16bppGrayHalf, {1, Sample::Half, 0, 0, 0, -1, false}},
{&GUID_PKPixelFormat48bppRGBHalf, {3, Sample::Half, 0, 1, 2, -1, false}},
{&GUID_PKPixelFormat64bppRGBHalf, {4, Sample::Half, 0, 1, 2, -1, false}},
{&GUID_PKPixelFormat64bppRGBAHalf, {4, Sample::Half, 0, 1, 2, 3, false}},
{&GUID_PKPixelFormat32bppGrayFloat, {1, Sample::Float, 0, 0, 0, -1, false}},
{&GUID_PKPixelFormat96bppRGBFloat, {3, Sample::Float, 0, 1, 2, -1, false}},
{&GUID_PKPixelFormat128bppRGBFloat, {4, Sample::Float, 0, 1, 2, -1, false}},
{&GUID_PKPixelFormat128bppRGBAFloat, {4, Sample::Float, 0, 1, 2, 3, false}},
{&GUID_PKPixelFormat128bppPRGBAFloat, {4, Sample::Float, 0, 1, 2, 3, true}},
{&GUID_PKPixelFormat16bppGrayFixedPoint,
{1, Sample::Fixed16, 0, 0, 0, -1, false}},
{&GUID_PKPixelFormat48bppRGBFixedPoint,
{3, Sample::Fixed16, 0, 1, 2, -1, false}},
{&GUID_PKPixelFormat64bppRGBFixedPoint,
{4, Sample::Fixed16, 0, 1, 2, -1, false}},
{&GUID_PKPixelFormat64bppRGBAFixedPoint,
{4, Sample::Fixed16, 0, 1, 2, 3, false}},
{&GUID_PKPixelFormat32bppGrayFixedPoint,
{1, Sample::Fixed32, 0, 0, 0, -1, false}},
{&GUID_PKPixelFormat96bppRGBFixedPoint,
{3, Sample::Fixed32, 0, 1, 2, -1, false}},
{&GUID_PKPixelFormat128bppRGBFixedPoint,
{4, Sample::Fixed32, 0, 1, 2, -1, false}},
{&GUID_PKPixelFormat128bppRGBAFixedPoint,
{4, Sample::Fixed32, 0, 1, 2, 3, false}},
{&GUID_PKPixelFormat32bppRGBE, {4, Sample::Rgbe, 0, 1, 2, -1, false}},
};
// What to ask the format converter for when the file's own format is not in
// the table above. jxrlib converts into a bare handful of formats, and only
// these three are worth having: the first keeps all 16 bits, the second keeps
// alpha, the third is the widest sink it has. Whatever lands on the third
// loses precision--but that is the HDR family, and the working format is
// fixed-point BGRA16, so none of it could have been kept whole anyway.
// alpha, the third is the widest sink it has.
static const PKPixelFormatGUID *const kConversionTargets[] = {
&GUID_PKPixelFormat48bppRGB,
&GUID_PKPixelFormat32bppRGBA,
@@ -110,24 +152,102 @@ find_layout(const PKPixelFormatGUID &format)
return nullptr;
}
static size_t
sample_size(Sample type)
{
switch (type) {
case Sample::Unorm16:
case Sample::Half:
case Sample::Fixed16:
return 2;
case Sample::Float:
case Sample::Fixed32:
return 4;
default:
return 1;
}
}
// jxrlib decodes into host-endian words, and so does everything else here--
// the working format is only nominally little-endian. Rows of 16-bit samples
// always start on an even offset, so the load stays aligned.
// the working format is only nominally little-endian.
static uint16_t
sample(const uint8_t *pixel, const Layout &layout, int index)
{
if (layout.bits == 8)
if (layout.type == Sample::Unorm8)
return scale_nbit_to_u16(pixel[index], 8);
uint16_t value = 0;
memcpy(&value, pixel + size_t(index) * sizeof value, sizeof value);
return value;
}
static float
half_to_float(uint16_t half)
{
const int exponent = half >> 10 & 0x1f, mantissa = half & 0x3ff;
float value = INFINITY;
if (!exponent)
value = ldexp(float(mantissa), -24);
else if (exponent < 31)
value = ldexp(float(mantissa | 0x400), exponent - 25);
else if (mantissa)
value = NAN;
return half & 0x8000 ? -value : value;
}
// The float family, as the jxrlib converters that it has would read it.
static float
float_sample(const uint8_t *pixel, const Layout &layout, int index)
{
const uint8_t *p = pixel + size_t(index) * sample_size(layout.type);
uint16_t u16 = 0;
uint32_t u32 = 0;
switch (layout.type) {
case Sample::Half:
memcpy(&u16, p, sizeof u16);
return half_to_float(u16);
case Sample::Fixed16:
memcpy(&u16, p, sizeof u16);
return float(int16_t(u16)) / (1 << 13);
case Sample::Float:
memcpy(&u32, p, sizeof u32);
return bit_cast<float>(u32);
case Sample::Fixed32:
memcpy(&u32, p, sizeof u32);
return float(double(int32_t(u32)) / (1 << 24));
case Sample::Rgbe:
return pixel[3] ? ldexp(float(pixel[index]), pixel[3] - 128 - 8) : 0;
default:
return 0;
}
}
// scRGB puts 1.0 at SDR white by convention, and its colours may step out of
// its primaries, which are BT.709's.
static bool
split_pixels(const Layout &layout, const uint8_t *src, size_t stride,
Image &out, const OpenContext &ctx, Error *error)
{
const size_t pixel_size = size_t(layout.samples) * sample_size(layout.type);
vector<float> rgba(size_t(out.width) * out.height * 4);
for (uint32_t y = 0; y < out.height; y++) {
const uint8_t *s = src + size_t(y) * stride;
float *d = &rgba[size_t(y) * out.width * 4];
for (uint32_t x = 0; x < out.width; x++, s += pixel_size, d += 4) {
d[0] = float_sample(s, layout, layout.r);
d[1] = float_sample(s, layout, layout.g);
d[2] = float_sample(s, layout, layout.b);
d[3] = layout.a < 0 ? 1 : float_sample(s, layout, layout.a);
}
}
return split_hdr(
out, ctx, rgba, layout.premultiplied, kRec709Primaries, error);
}
static void
write_pixels(
const Layout &layout, const uint8_t *src, size_t stride, Image &out)
{
size_t pixel_size = size_t(layout.samples) * (layout.bits / 8);
size_t pixel_size = size_t(layout.samples) * sample_size(layout.type);
for (uint32_t y = 0; y < out.height; y++) {
const uint8_t *s = src + size_t(y) * stride;
uint16_t *d = row_u16(out, y);
@@ -231,7 +351,7 @@ decode_image(JxrLoadContext &ctx, const OpenContext &octx, Error *error)
// The converter works in place: the buffer first receives the decoder's
// own pixels and only then becomes the converted ones, so it has to fit
// whichever of the two formats is the wider.
uint32_t bits = uint32_t(layout->samples) * layout->bits;
uint32_t bits = uint32_t(layout->samples * sample_size(layout->type) * 8);
if (ctx.converter)
bits = max(bits, bits_per_pixel(format));
@@ -253,8 +373,6 @@ decode_image(JxrLoadContext &ctx, const OpenContext &octx, Error *error)
return nullptr;
}
write_pixels(*layout, buffer.data(), stride, *image);
U32 icc = 0;
if (!ctx.decoder->GetColorContext(ctx.decoder, nullptr, &icc) && icc) {
image->icc.resize(icc);
@@ -262,7 +380,16 @@ decode_image(JxrLoadContext &ctx, const OpenContext &octx, Error *error)
image->icc.clear();
}
finish_image(*image, octx, nullptr, layout->premultiplied);
// split_hdr() leaves its base straight, in the profile it names.
if (layout->type < Sample::Half) {
write_pixels(*layout, buffer.data(), stride, *image);
finish_image(*image, octx, nullptr, layout->premultiplied);
} else if (split_pixels(
*layout, buffer.data(), stride, *image, octx, error)) {
finish_image(*image, octx, image->effective_profile.get(), false);
} else {
return nullptr;
}
return image;
}
+66 -11
View File
@@ -179,6 +179,8 @@ struct WuffsLoadContext {
bool have_srgb = false; ///< PNG sRGB chunk seen
optional<double> gamma; ///< Decoding exponent from PNG gAMA
optional<array<double, 8>> chrm; ///< PNG cHRM: white xy, then R, G, B xy
uint8_t hdr_transfer = 0; ///< PNG cICP transfer, when PQ or HLG
double hdr_primaries[6] = {}; ///< PNG cICP primaries, likewise
const OpenContext *octx = nullptr; ///< Caller-supplied context
shared_ptr<Cmm> cmm; ///< CMM context, never null
@@ -422,9 +424,24 @@ load_wuffs_frame(WuffsLoadContext &ctx, Error *error)
// swizzler does not support every source format as a direct premultiplied
// destination (e.g. 16-bit-per-channel truecolour PNG). Colour-manage
// (if applicable) and premultiply now, before any compositing.
if (ctx.source)
// split_hdr() leaves its base straight, in the profile it names.
// Maps are per page, and only still images have them.
if (ctx.hdr_transfer) {
OpenContext octx = *ctx.octx;
octx.gain_maps =
octx.gain_maps && !wuffs_base__frame_config__index(&fc);
// HLG's nominal display, as BT.2408 has it.
if (!split_hdr_signal(*image, octx, ctx.hdr_transfer, ctx.hdr_primaries,
1000, false, error)) {
ctx.result.reset();
ctx.result_tail.reset();
return false;
}
} else if (ctx.source) {
image->effective_profile = ctx.source;
finish_image(*image, *ctx.octx, ctx.source.get(), /*input_premul=*/false);
}
finish_image(*image, *ctx.octx, image->effective_profile.get(),
/*input_premul=*/false);
// Single-frame images get a fast path, animations are handled slowly.
if (wuffs_base__frame_config__index(&fc) > 0 &&
@@ -457,10 +474,42 @@ load_wuffs_frame(WuffsLoadContext &ctx, Error *error)
return ok;
}
// PNG cICP, which Wuffs does not parse, and which has to come before IDAT:
// its colour primaries and transfer characteristics, for RGB in full range.
static bool
png_cicp(span<const uint8_t> data, uint8_t *primaries, uint8_t *transfer)
{
static const uint8_t signature[] = {
0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'};
if (data.size() < sizeof signature ||
memcmp(data.data(), signature, sizeof signature))
return false;
for (size_t at = sizeof signature; data.size() - at >= 12;) {
const uint8_t *chunk = data.data() + at;
const size_t length = size_t(chunk[0]) << 24 | size_t(chunk[1]) << 16 |
size_t(chunk[2]) << 8 | chunk[3];
if (!memcmp(chunk + 4, "IDAT", 4) || data.size() - at - 12 < length)
return false;
if (!memcmp(chunk + 4, "cICP", 4)) {
if (length != 4 || chunk[10] || !chunk[11])
return false;
*primaries = chunk[8];
*transfer = chunk[9];
return true;
}
at += 12 + length;
}
return false;
}
static ImagePtr
open_wuffs(wuffs_base__image_decoder *dec, wuffs_base__io_buffer src,
open_wuffs(wuffs_base__image_decoder *dec, span<const uint8_t> data,
const OpenContext &octx, Error *error)
{
wuffs_base__io_buffer src =
wuffs_base__ptr_u8__reader((uint8_t *) data.data(), data.size(), true);
WuffsLoadContext ctx;
ctx.dec = dec;
ctx.src = &src;
@@ -468,8 +517,6 @@ open_wuffs(wuffs_base__image_decoder *dec, wuffs_base__io_buffer src,
ctx.cmm = cmm_or_default(octx);
ctx.target = octx.screen_profile.get();
// TODO(p): See if something could and should be done about
// https://www.w3.org/TR/png-hdr-pq/
wuffs_base__image_decoder__set_report_metadata(
ctx.dec, WUFFS_BASE__FOURCC__EXIF, true);
wuffs_base__image_decoder__set_report_metadata(
@@ -514,9 +561,15 @@ open_wuffs(wuffs_base__image_decoder *dec, wuffs_base__io_buffer src,
}
// PNG (3rd edition) Table 1: iCCP outranks sRGB, which outranks cHRM and
// gAMA; lower-priority chunks are to be ignored. cICP, which would come
// first, is not parsed by Wuffs. A missing half of the cHRM/gAMA pair is
// filled in from sRGB, as the specification says nothing about halves.
// gAMA; lower-priority chunks are to be ignored. cICP comes first, but
// only PQ and HLG are acted on (https://www.w3.org/TR/png-hdr-pq/), as
// honouring the rest would change how existing SDR images look.
// A missing half of the cHRM/gAMA pair is filled in from sRGB,
// as the specification says nothing about halves.
uint8_t cicp_code = 0, cicp_transfer = 0;
if (png_cicp(data, &cicp_code, &cicp_transfer) &&
cicp_hdr(cicp_code, cicp_transfer, ctx.hdr_primaries))
ctx.hdr_transfer = cicp_transfer;
if (ctx.have_iccp)
ctx.source = ctx.cmm->get_profile(ctx.meta_iccp);
if (!ctx.source && ctx.have_srgb)
@@ -547,6 +600,10 @@ open_wuffs(wuffs_base__image_decoder *dec, wuffs_base__io_buffer src,
if (octx.first_frame_only)
break;
// The first frame only learns that it is not a still one here.
if (ctx.result && ctx.result->frame_next)
ctx.result->gain_map.reset();
apply_collected_metadata(ctx);
if (!ctx.result && error && error->message.empty())
set_error(error, _("no frames decoded"));
@@ -564,9 +621,7 @@ open_wuffs_using(wuffs_base__image_decoder *(*allocate)(),
return nullptr;
}
wuffs_base__io_buffer src =
wuffs_base__ptr_u8__reader((uint8_t *) data.data(), data.size(), true);
return open_wuffs(dec.get(), src, ctx, error);
return open_wuffs(dec.get(), data, ctx, error);
}
// --- Public entry points -----------------------------------------------------
+342 -178
View File
@@ -36,10 +36,18 @@ constexpr VkDeviceSize kTileBytesPerPixel = 8;
constexpr VkFormat kTileFormat = VK_FORMAT_R16G16B16A16_UNORM;
constexpr VkFormat kMidFormat = VK_FORMAT_R16G16B16A16_SFLOAT;
constexpr VkFormat kGainFormat = VK_FORMAT_R16_UNORM;
namespace
{
// A host-visible buffer, mapped, that uploads copy out of.
struct Staging {
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
uint8_t *mapped = nullptr;
};
struct TileRect {
int32_t ox = 0, oy = 0, w = 0, h = 0;
};
@@ -69,11 +77,15 @@ struct PushConstants {
float bg_r = 0, bg_g = 0, bg_b = 0;
float checker_r = 0, checker_g = 0, checker_b = 0;
float checker_size = 1;
// The gain map's metadata folded with its weight: (max - min) * weight,
// min * weight, 1 / gamma, offset. Both scaled terms are zero exactly
// when there is nothing to apply.
float gain_scale = 0, gain_min = 0, gain_inv_gamma = 1, gain_offset = 0;
};
} // namespace
static_assert(sizeof(PushConstants) == 96);
static_assert(sizeof(PushConstants) == 112);
// TODO(p): What in Hell could be a reason for being this lenient?
constexpr float kAngleFast = 1e-5f;
@@ -165,19 +177,36 @@ struct ScaleEngine::Impl {
uint32_t grid_rows = 1;
bool image_opaque = true;
// A 1×1 stand-in when there is no map, since the tile set binds one.
VkImage gain_image = VK_NULL_HANDLE;
VkDeviceMemory gain_memory = VK_NULL_HANDLE;
VkImageView gain_view = VK_NULL_HANDLE;
bool gain_mapped = false;
float gain_min = 0, gain_max = 0, gain_gamma = 1, gain_offset = 0;
bool ready = false;
bool create_mid(string *error);
void destroy_mid();
void destroy_tiles();
void destroy_gain();
void destroy_pipeline();
void destroy_all();
bool create_pipeline_objects(string *error);
static vector<TileRect> split_grid(
uint32_t w, uint32_t h, uint32_t cols, uint32_t rows, string *error);
bool submit_upload(auto &&record, string *error);
bool create_sampled(VkFormat format, uint32_t w, uint32_t h,
uint32_t layers, VkImageViewType view_type, VkComponentMapping swizzle,
VkImage *image, VkDeviceMemory *memory, VkImageView *view,
string *error);
bool create_staging(VkDeviceSize bytes, Staging &staging, string *error);
void destroy_staging(Staging &staging);
void record_upload_layout(VkImage image, uint32_t layers, bool sampled);
bool upload_tiles(const uint8_t *pixels, size_t stride,
const vector<TileRect> &rects, string *error);
bool upload_gain(
const uint16_t *texels, uint32_t w, uint32_t h, string *error);
void write_descriptors();
bool ensure_mid(uint32_t vp_w, uint32_t src_h, string *error);
PushConstants make_push(const ScaleView &view, uint32_t vp_w, uint32_t vp_h,
@@ -247,6 +276,24 @@ ScaleEngine::Impl::destroy_tiles()
// replacing GPU tiles; dimensions stay owned by set_image.
}
void
ScaleEngine::Impl::destroy_gain()
{
if (!device)
return;
if (gain_view)
vkDestroyImageView(device, gain_view, nullptr);
if (gain_image)
vkDestroyImage(device, gain_image, nullptr);
if (gain_memory)
vkFreeMemory(device, gain_memory, nullptr);
gain_view = VK_NULL_HANDLE;
gain_image = VK_NULL_HANDLE;
gain_memory = VK_NULL_HANDLE;
gain_mapped = false;
}
void
ScaleEngine::Impl::destroy_pipeline()
{
@@ -287,6 +334,7 @@ ScaleEngine::Impl::destroy_all()
vkDeviceWaitIdle(device);
destroy_mid();
destroy_tiles();
destroy_gain();
destroy_pipeline();
if (dset_pool) {
vkDestroyDescriptorPool(device, dset_pool, nullptr);
@@ -522,6 +570,139 @@ ScaleEngine::Impl::submit_upload(auto &&record, string *error)
return CALL_VK(QueueWaitIdle, "", queue);
}
// A device-local image that transfers fill and shaders sample, with its view.
bool
ScaleEngine::Impl::create_sampled(VkFormat format, uint32_t w, uint32_t h,
uint32_t layers, VkImageViewType view_type, VkComponentMapping swizzle,
VkImage *image, VkDeviceMemory *memory, VkImageView *view, string *error)
{
VkImageCreateInfo ici{
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
.imageType = VK_IMAGE_TYPE_2D,
.format = format,
.extent = {w, h, 1},
.mipLevels = 1,
.arrayLayers = layers,
.samples = VK_SAMPLE_COUNT_1_BIT,
.tiling = VK_IMAGE_TILING_OPTIMAL,
.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
};
if (!CALL_VK(CreateImage, " upload", device, &ici, nullptr, image))
return false;
VkMemoryRequirements mr{};
vkGetImageMemoryRequirements(device, *image, &mr);
const uint32_t mem_type = vk_memory_type(phys, mr.memoryTypeBits,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, error, nullptr);
if (mem_type == UINT32_MAX)
return false;
VkMemoryAllocateInfo mai{
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
.allocationSize = mr.size,
.memoryTypeIndex = mem_type,
};
if (!CALL_VK(AllocateMemory, " upload", device, &mai, nullptr, memory) ||
!CALL_VK(BindImageMemory, " upload", device, *image, *memory, 0))
return false;
VkImageViewCreateInfo vi{
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
.image = *image,
.viewType = view_type,
.format = format,
.components = swizzle,
.subresourceRange =
{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.levelCount = 1,
.layerCount = layers,
},
};
return CALL_VK(CreateImageView, " upload", device, &vi, nullptr, view);
}
// Leaves what it has made for destroy_staging(), also on failure.
bool
ScaleEngine::Impl::create_staging(
VkDeviceSize bytes, Staging &staging, string *error)
{
VkBufferCreateInfo bci{
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.size = bytes,
.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
};
if (!CALL_VK(
CreateBuffer, " staging", device, &bci, nullptr, &staging.buffer))
return false;
VkMemoryRequirements mr{};
vkGetBufferMemoryRequirements(device, staging.buffer, &mr);
const uint32_t mem_type = vk_memory_type(phys, mr.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
error, nullptr);
if (mem_type == UINT32_MAX)
return false;
VkMemoryAllocateInfo mai{
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
.allocationSize = mr.size,
.memoryTypeIndex = mem_type,
};
return CALL_VK(AllocateMemory, " staging", device, &mai, nullptr,
&staging.memory) &&
CALL_VK(BindBufferMemory, " staging", device, staging.buffer,
staging.memory, 0) &&
CALL_VK(MapMemory, " staging", device, staging.memory, 0, bytes, 0,
(void **) &staging.mapped);
}
// Freeing the memory unmaps it.
void
ScaleEngine::Impl::destroy_staging(Staging &staging)
{
if (staging.buffer)
vkDestroyBuffer(device, staging.buffer, nullptr);
if (staging.memory)
vkFreeMemory(device, staging.memory, nullptr);
staging = {};
}
// Uploads move an image from creation to transfers, then to sampling.
void
ScaleEngine::Impl::record_upload_layout(
VkImage image, uint32_t layers, bool sampled)
{
VkImageMemoryBarrier barrier{
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.srcAccessMask = sampled ? VkAccessFlags(VK_ACCESS_TRANSFER_WRITE_BIT)
: VkAccessFlags(0),
.dstAccessMask =
sampled ? VK_ACCESS_SHADER_READ_BIT : VK_ACCESS_TRANSFER_WRITE_BIT,
.oldLayout = sampled ? VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
: VK_IMAGE_LAYOUT_UNDEFINED,
.newLayout = sampled ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
: VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = image,
.subresourceRange =
{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.levelCount = 1,
.layerCount = layers,
},
};
vkCmdPipelineBarrier(upload_cmd,
sampled ? VK_PIPELINE_STAGE_TRANSFER_BIT
: VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
sampled ? VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT
: VK_PIPELINE_STAGE_TRANSFER_BIT,
0, 0, nullptr, 0, nullptr, 1, &barrier);
}
bool
ScaleEngine::Impl::upload_tiles(const uint8_t *pixels, size_t stride,
const vector<TileRect> &rects, string *error)
@@ -563,154 +744,27 @@ ScaleEngine::Impl::upload_tiles(const uint8_t *pixels, size_t stride,
return false;
}
VkImageCreateInfo ici{
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
.imageType = VK_IMAGE_TYPE_2D,
.format = kTileFormat,
.extent = {tile_pad_w, tile_pad_h, 1},
.mipLevels = 1,
.arrayLayers = tile_count,
.samples = VK_SAMPLE_COUNT_1_BIT,
.tiling = VK_IMAGE_TILING_OPTIMAL,
.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
};
if (!CALL_VK(CreateImage, " tiles", device, &ici, nullptr, &tile_image))
if (!create_sampled(kTileFormat, tile_pad_w, tile_pad_h, tile_count,
VK_IMAGE_VIEW_TYPE_2D_ARRAY,
{VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_G,
VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_A},
&tile_image, &tile_memory, &tile_view, error))
return false;
VkMemoryRequirements mr{};
vkGetImageMemoryRequirements(device, tile_image, &mr);
uint32_t mem_type = vk_memory_type(phys, mr.memoryTypeBits,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, error, nullptr);
if (mem_type == UINT32_MAX)
return false;
VkMemoryAllocateInfo mai{
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
.allocationSize = mr.size,
.memoryTypeIndex = mem_type,
};
if (!CALL_VK(AllocateMemory, " tiles", device, &mai, nullptr, &tile_memory))
return false;
if (!CALL_VK(BindImageMemory, " tiles", device, tile_image, tile_memory, 0))
return false;
VkImageViewCreateInfo vi{
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
.image = tile_image,
.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY,
.format = kTileFormat,
.components =
{
.r = VK_COMPONENT_SWIZZLE_B,
.g = VK_COMPONENT_SWIZZLE_G,
.b = VK_COMPONENT_SWIZZLE_R,
.a = VK_COMPONENT_SWIZZLE_A,
},
.subresourceRange =
{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.levelCount = 1,
.layerCount = tile_count,
},
};
if (!CALL_VK(CreateImageView, " tiles", device, &vi, nullptr, &tile_view))
return false;
const VkDeviceSize staging_bytes =
VkDeviceSize(tile_pad_w) * tile_pad_h * kTileBytesPerPixel;
VkBuffer staging = VK_NULL_HANDLE;
VkDeviceMemory staging_mem = VK_NULL_HANDLE;
VkBufferCreateInfo bci{
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.size = staging_bytes,
.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
};
if (!CALL_VK(CreateBuffer, " staging", device, &bci, nullptr, &staging))
return false;
VkMemoryRequirements smr{};
vkGetBufferMemoryRequirements(device, staging, &smr);
mem_type = vk_memory_type(phys, smr.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
error, nullptr);
if (mem_type == UINT32_MAX) {
vkDestroyBuffer(device, staging, nullptr);
return false;
}
VkMemoryAllocateInfo smai{
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
.allocationSize = smr.size,
.memoryTypeIndex = mem_type,
};
if (!CALL_VK(
AllocateMemory, " staging", device, &smai, nullptr, &staging_mem)) {
vkDestroyBuffer(device, staging, nullptr);
return false;
}
if (!CALL_VK(
BindBufferMemory, " staging", device, staging, staging_mem, 0)) {
vkDestroyBuffer(device, staging, nullptr);
vkFreeMemory(device, staging_mem, nullptr);
return false;
}
uint8_t *mapped = nullptr;
if (!CALL_VK(MapMemory, " staging", device, staging_mem, 0, staging_bytes,
0, (void **) &mapped)) {
vkDestroyBuffer(device, staging, nullptr);
vkFreeMemory(device, staging_mem, nullptr);
return false;
}
auto transition = [&](VkImageLayout from, VkImageLayout to,
VkAccessFlags src_access, VkAccessFlags dst_access,
VkPipelineStageFlags src_stage,
VkPipelineStageFlags dst_stage) -> bool {
return submit_upload(
[&] {
VkImageMemoryBarrier barrier{
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.srcAccessMask = src_access,
.dstAccessMask = dst_access,
.oldLayout = from,
.newLayout = to,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = tile_image,
.subresourceRange =
{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.levelCount = 1,
.layerCount = tile_count,
},
};
vkCmdPipelineBarrier(upload_cmd, src_stage, dst_stage, 0, 0,
nullptr, 0, nullptr, 1, &barrier);
},
Staging staging;
bool ok = create_staging(
VkDeviceSize(tile_pad_w) * tile_pad_h * kTileBytesPerPixel,
staging, error) &&
submit_upload(
[&] { record_upload_layout(tile_image, tile_count, false); },
error);
};
if (!transition(VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT)) {
vkUnmapMemory(device, staging_mem);
vkDestroyBuffer(device, staging, nullptr);
vkFreeMemory(device, staging_mem, nullptr);
return false;
}
for (uint32_t i = 0; i < tile_count; i++) {
for (uint32_t i = 0; ok && i < tile_count; i++) {
const TileRect &r = tile_rects[i];
const size_t row_bytes = size_t(r.w) * kTileBytesPerPixel;
for (int32_t y = 0; y < r.h; y++) {
const uint8_t *src = pixels + size_t(r.oy + y) * stride +
size_t(r.ox) * kTileBytesPerPixel;
memcpy(mapped + size_t(y) * row_bytes, src, row_bytes);
memcpy(staging.mapped + size_t(y) * row_bytes, src, row_bytes);
}
VkBufferImageCopy copy{
.bufferOffset = 0,
@@ -723,35 +777,20 @@ ScaleEngine::Impl::upload_tiles(const uint8_t *pixels, size_t stride,
},
.imageExtent = {uint32_t(r.w), uint32_t(r.h), 1},
};
if (!submit_upload(
[&] {
vkCmdCopyBufferToImage(upload_cmd, staging, tile_image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copy);
},
error)) {
vkUnmapMemory(device, staging_mem);
vkDestroyBuffer(device, staging, nullptr);
vkFreeMemory(device, staging_mem, nullptr);
return false;
}
ok = submit_upload(
[&] {
vkCmdCopyBufferToImage(upload_cmd, staging.buffer, tile_image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copy);
},
error);
}
if (!transition(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT)) {
vkUnmapMemory(device, staging_mem);
vkDestroyBuffer(device, staging, nullptr);
vkFreeMemory(device, staging_mem, nullptr);
return false;
}
vkUnmapMemory(device, staging_mem);
vkDestroyBuffer(device, staging, nullptr);
vkFreeMemory(device, staging_mem, nullptr);
write_descriptors();
return true;
ok = ok &&
submit_upload(
[&] { record_upload_layout(tile_image, tile_count, true); }, error);
destroy_staging(staging);
if (ok)
write_descriptors();
return ok;
}
void
@@ -791,6 +830,60 @@ ScaleEngine::Impl::write_descriptors()
}
}
// Replaces the gain map image, and binds it to the tile set.
bool
ScaleEngine::Impl::upload_gain(
const uint16_t *texels, uint32_t w, uint32_t h, string *error)
{
destroy_gain();
if (!create_sampled(kGainFormat, w, h, 1, VK_IMAGE_VIEW_TYPE_2D, {},
&gain_image, &gain_memory, &gain_view, error))
return false;
const VkDeviceSize bytes = VkDeviceSize(w) * h * sizeof *texels;
Staging staging;
bool ok = create_staging(bytes, staging, error);
if (ok) {
memcpy(staging.mapped, texels, bytes);
VkBufferImageCopy copy{
.imageSubresource =
{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.layerCount = 1,
},
.imageExtent = {w, h, 1},
};
ok = submit_upload(
[&] {
record_upload_layout(gain_image, 1, false);
vkCmdCopyBufferToImage(upload_cmd, staging.buffer, gain_image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copy);
record_upload_layout(gain_image, 1, true);
},
error);
}
destroy_staging(staging);
if (!ok)
return false;
// The vertical pass never reads the map, so the mid set goes without.
VkDescriptorImageInfo info{
.sampler = sampler,
.imageView = gain_view,
.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
};
VkWriteDescriptorSet write{
.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
.dstSet = dset_tiles,
.dstBinding = 2,
.descriptorCount = 1,
.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
.pImageInfo = &info,
};
vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
return true;
}
bool
ScaleEngine::Impl::ensure_mid(uint32_t vp_w, uint32_t src_h, string *error)
{
@@ -971,7 +1064,15 @@ ScaleEngine::Impl::make_push(const ScaleView &view, uint32_t vp_w,
(view.checkerboard ? (1 << 16) : 0) | (view.composite ? (1 << 17) : 0) |
(image_opaque ? (1 << 18) : 0) |
(view.nonlinear_processing ? (1 << 19) : 0) |
(view.output_encoding == ScaleEncoding::Linear ? (1 << 20) : 0);
(view.output_encoding == ScaleEncoding::Linear ? (1 << 20) : 0) |
(view.hdr ? (1 << 21) : 0);
// Gain is only meaningful in linear light.
if (gain_mapped && view.gain_weight > 0 && !view.nonlinear_processing) {
pc.gain_scale = (gain_max - gain_min) * view.gain_weight;
pc.gain_min = gain_min * view.gain_weight;
pc.gain_inv_gamma = 1 / gain_gamma;
pc.gain_offset = gain_offset;
}
// Backgrounds arrive encoded, independently of the output attachment.
auto background = [&](array<float, 3> rgb) {
if (view.nonlinear_processing)
@@ -1164,10 +1265,20 @@ ScaleEngine::init(VkPhysicalDevice phys, VkDevice device, VkQueue queue,
impl_ = new Impl();
Impl &e = *impl_;
if (e.ready && e.phys == phys && e.device == device &&
e.dest_format == dest_format &&
e.dest_final_layout == dest_final_layout)
return true;
if (e.ready && e.phys == phys && e.device == device) {
if (e.dest_format == dest_format &&
e.dest_final_layout == dest_final_layout)
return true;
// Framebuffers of the mid array stay compatible with the new passes.
vkDeviceWaitIdle(device);
e.dest_format = dest_format;
e.dest_final_layout = dest_final_layout;
if (e.create_pipeline_objects(error))
return true;
e.destroy_all();
return false;
}
e.destroy_all();
e.phys = phys;
@@ -1229,10 +1340,12 @@ ScaleEngine::init(VkPhysicalDevice phys, VkDevice device, VkQueue queue,
VK_SHADER_STAGE_FRAGMENT_BIT, nullptr},
{1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_FRAGMENT_BIT,
nullptr},
{2, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1,
VK_SHADER_STAGE_FRAGMENT_BIT, nullptr},
};
VkDescriptorSetLayoutCreateInfo dlci{
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
.bindingCount = 2,
.bindingCount = 3,
.pBindings = bindings,
};
if (!CALL_VK(CreateDescriptorSetLayout, " scale", device, &dlci, nullptr,
@@ -1242,7 +1355,7 @@ ScaleEngine::init(VkPhysicalDevice phys, VkDevice device, VkQueue queue,
}
const VkDescriptorPoolSize pool_sizes[] = {
{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2},
{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 4},
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2},
};
VkDescriptorPoolCreateInfo dpci{
@@ -1285,7 +1398,8 @@ ScaleEngine::init(VkPhysicalDevice phys, VkDevice device, VkQueue queue,
}
e.ready = true;
if (set_encoding(profile_encoding(nullptr), error))
if (set_encoding(profile_encoding(nullptr), error) &&
set_gain_map(nullptr, error))
return true;
e.destroy_all();
return false;
@@ -1384,7 +1498,8 @@ ScaleEngine::set_image(
if (rects.empty())
return false;
return e.upload_tiles(pixels, stride, rects, error);
return e.upload_tiles(pixels, stride, rects, error) &&
(!e.gain_mapped || set_gain_map(nullptr, error));
}
void
@@ -1396,6 +1511,55 @@ ScaleEngine::clear_image()
impl_->image_w = impl_->image_h = 0;
impl_->grid_cols = impl_->grid_rows = 1;
impl_->destroy_mid();
if (impl_->gain_mapped)
(void) set_gain_map(nullptr, nullptr);
}
bool
ScaleEngine::set_gain_map(const GainMap *map, string *error)
{
if (!impl_ || !impl_->ready) {
if (error)
*error = "ScaleEngine not initialized";
return false;
}
Impl &e = *impl_;
if (!map || map->data.size() != size_t(map->width) * map->height ||
!map->width || !map->height) {
const uint16_t none = 0;
return e.upload_gain(&none, 1, 1, error);
}
// Box-filter a map the device cannot hold down to one it can.
uint32_t w = map->width, h = map->height;
vector<uint16_t> reduced;
const uint32_t factor = ceil_div(max(w, h), e.max_image_dim_2d);
if (factor > 1) {
w = ceil_div(w, factor);
h = ceil_div(h, factor);
reduced.resize(size_t(w) * h);
for (uint32_t y = 0; y < h; y++)
for (uint32_t x = 0; x < w; x++) {
uint64_t sum = 0, n = 0;
for (uint32_t sy = y * factor;
sy < min((y + 1) * factor, map->height); sy++)
for (uint32_t sx = x * factor;
sx < min((x + 1) * factor, map->width); sx++, n++)
sum += map->data[size_t(sy) * map->width + sx];
reduced[size_t(y) * w + x] = uint16_t((sum + n / 2) / n);
}
}
if (!e.upload_gain(
reduced.empty() ? map->data.data() : reduced.data(), w, h, error))
return false;
e.gain_mapped = true;
e.gain_min = map->min;
e.gain_max = map->max;
e.gain_gamma = map->gamma;
e.gain_offset = map->offset;
return true;
}
uint32_t
+12 -1
View File
@@ -55,6 +55,12 @@ struct ScaleView {
/// Clear it to keep premultiplied alpha, as offscreen readback needs.
bool composite = false;
Filter filter = Filter::Bilinear;
/// The HDR rendition: values above SDR white survive filtering.
/// Needs linear processing, which is then what `nonlinear_processing`
/// must say.
bool hdr = false;
/// set_gain_map()'s weight, see gain_map_weight(). Zero skips the map.
float gain_weight = 0.f;
};
/// Shared H→V tile scale engine. Does not own VkInstance/VkDevice/VkQueue.
@@ -72,7 +78,8 @@ public:
/// `dest_final_layout` is `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR` for swapchain
/// targets, or `VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL` for offscreen
/// readback.
/// readback. Called again with another destination, it rebuilds only
/// the render pass and pipelines, and keeps the image.
bool init(VkPhysicalDevice phys, VkDevice device, VkQueue queue,
uint32_t queue_family, VkFormat dest_format,
VkImageLayout dest_final_layout, std::string *error);
@@ -83,9 +90,13 @@ public:
bool set_encoding(const ProfileEncoding &encoding, std::string *error);
VkDescriptorBufferInfo encoding_buffer() const;
/// Also unbinds any gain map.
bool set_image(uint32_t w, uint32_t h, const uint8_t *pixels, size_t stride,
std::string *error);
void clear_image();
/// The image's gain map, applied per tap in linear light, or null.
/// Maps over maxImageDimension2D are downscaled.
bool set_gain_map(const GainMap *map, std::string *error);
[[nodiscard]] uint32_t image_width() const;
[[nodiscard]] uint32_t image_height() const;
+78 -6
View File
@@ -47,6 +47,8 @@ pub enum dnrs_pixel_format {
GrayAlpha16Le,
Rgb16Le,
Rgba16Le,
RgbF32,
RgbaF32,
}
#[repr(C)]
@@ -206,6 +208,8 @@ fn bytes_per_pixel(format: dnrs_pixel_format) -> usize {
dnrs_pixel_format::GrayAlpha16Le => 4,
dnrs_pixel_format::Rgb16Le => 6,
dnrs_pixel_format::Rgba16Le => 8,
dnrs_pixel_format::RgbF32 => 12,
dnrs_pixel_format::RgbaF32 => 16,
}
}
@@ -273,6 +277,41 @@ fn frame_from_dynamic(
make_frame(width, height, format, pixels, duration_ms)
}
// OpenEXR, Radiance and float TIFF carry linear light, which libdn splits into
// an SDR base and a gain map, so their floats go out unclipped. Associated
// alpha, as OpenEXR's always is, goes out straight, as every other format's
// does. Anything else goes out as frame_from_dynamic() has it.
fn frame_from_float(
image: DynamicImage,
associated: bool,
) -> Result<Frame, String> {
let (width, height) = (image.width(), image.height());
let (format, pixels) = match image {
DynamicImage::ImageRgb32F(v) => {
(dnrs_pixel_format::RgbF32, f32_to_ne(v.into_raw()))
}
DynamicImage::ImageRgba32F(mut v) => {
if associated {
for pixel in v.pixels_mut() {
let alpha = pixel[3];
if alpha > 0.0 {
pixel[0] /= alpha;
pixel[1] /= alpha;
pixel[2] /= alpha;
}
}
}
(dnrs_pixel_format::RgbaF32, f32_to_ne(v.into_raw()))
}
other => return frame_from_dynamic(other, 0),
};
make_frame(width, height, format, pixels, 0)
}
fn f32_to_ne(samples: Vec<f32>) -> Vec<u8> {
samples.into_iter().flat_map(f32::to_ne_bytes).collect()
}
fn u16_to_le(samples: Vec<u16>) -> Vec<u8> {
let mut bytes = Vec::with_capacity(samples.len() * 2);
for sample in samples {
@@ -352,13 +391,20 @@ fn collect_metadata(
})
}
fn decode_frame<D: ImageDecoder>(
fn decode_dynamic<D: ImageDecoder>(
mut decoder: D,
) -> Result<(Frame, Metadata), String> {
) -> Result<(DynamicImage, Metadata), String> {
set_decoder_limits(&mut decoder)?;
let metadata = collect_metadata(&mut decoder)?;
let image =
DynamicImage::from_decoder(decoder).map_err(|e| e.to_string())?;
Ok((image, metadata))
}
fn decode_frame<D: ImageDecoder>(
decoder: D,
) -> Result<(Frame, Metadata), String> {
let (image, metadata) = decode_dynamic(decoder)?;
Ok((frame_from_dynamic(image, 0)?, metadata))
}
@@ -477,10 +523,12 @@ fn decode_tiff_pages(
.ok_or_else(|| "missing TIFF page offset".to_string())?
.0;
tiff_select_page(&mut page, offset)?;
let decoded = decode_still(ImageReader::with_format(
Cursor::new(page.as_slice()),
ImageFormat::Tiff,
))?;
let associated = scanner
.find_tag_unsigned_vec::<u16>(tiff::tags::Tag::ExtraSamples)
.ok()
.flatten()
.is_some_and(|extra| extra.first() == Some(&1));
let decoded = decode_float_still(&page, ImageFormat::Tiff, associated)?;
total = total
.checked_add(decoded.0.pixels.len() as u64)
.ok_or_else(|| "decoded image size overflow".to_string())?;
@@ -521,6 +569,27 @@ fn decode_still_frames(
Ok((vec![frame], metadata, 0))
}
fn decode_float_still(
data: &[u8],
format: ImageFormat,
associated: bool,
) -> Result<(Frame, Metadata), String> {
let mut reader = ImageReader::with_format(Cursor::new(data), format);
reader.limits(image_limits());
let (image, metadata) =
decode_dynamic(reader.into_decoder().map_err(|e| e.to_string())?)?;
Ok((frame_from_float(image, associated)?, metadata))
}
fn decode_float_frames(
data: &[u8],
format: ImageFormat,
) -> Result<(Vec<Frame>, Metadata, u64), String> {
let (frame, metadata) =
decode_float_still(data, format, format == ImageFormat::OpenExr)?;
Ok((vec![frame], metadata, 0))
}
fn decode_image_rs(
data: &[u8],
format: ImageFormat,
@@ -585,6 +654,9 @@ fn decode_image_rs(
decode_tiff_pages(data, first_frame_only)?,
)
}
ImageFormat::OpenExr | ImageFormat::Hdr => {
decode_float_frames(data, format)?
}
_ => decode_still_frames(data, format)?,
};
if format == ImageFormat::Png {
+3
View File
@@ -37,6 +37,9 @@ enum dnrs_pixel_format {
DNRS_PIXEL_GRAY_ALPHA16LE,
DNRS_PIXEL_RGB16LE,
DNRS_PIXEL_RGBA16LE,
// Host-endian linear light, 1.0 at SDR white, straight alpha.
DNRS_PIXEL_RGB32F,
DNRS_PIXEL_RGBA32F,
};
struct dnrs_document_info {
+88
View File
@@ -274,6 +274,44 @@ fn tiff_pages_are_separate() {
}
}
#[test]
fn float_tiff_keeps_linear_floats() {
use tiff::encoder::{colortype, TiffEncoder};
use tiff::tags::Tag;
let mut cursor = Cursor::new(Vec::new());
{
let mut encoder = TiffEncoder::new(&mut cursor).unwrap();
encoder
.write_image::<colortype::RGB32Float>(1, 1, &[4.0, -0.5, 0.25])
.unwrap();
}
assert_eq!(
float_frame(cursor.get_ref()),
(dnrs_pixel_format::RgbF32 as u32, vec![4.0, -0.5, 0.25])
);
// ExtraSamples says whether alpha is associated, and it then goes out
// straight. Unassociated alpha stays as it is.
for (extra, expected) in [(1_u16, [4.0, 2.0, 1.0]), (2, [2.0, 1.0, 0.5])] {
let mut cursor = Cursor::new(Vec::new());
{
let mut encoder = TiffEncoder::new(&mut cursor).unwrap();
let mut image =
encoder.new_image::<colortype::RGBA32Float>(1, 1).unwrap();
image
.encoder()
.write_tag(Tag::ExtraSamples, &[extra][..])
.unwrap();
image.write_data(&[2.0, 1.0, 0.5, 0.5]).unwrap();
}
let (format, samples) = float_frame(cursor.get_ref());
assert_eq!(format, dnrs_pixel_format::RgbaF32 as u32);
assert_eq!(samples[..3], expected);
assert_eq!(samples[3], 0.5);
}
}
#[test]
fn image_extras_xbm_and_xpm_are_sniffed() {
let xbm = br#"#define dot_width 1
@@ -395,6 +433,56 @@ fn image_rs_still_formats_are_decodable() {
verify_encoded_image(&data);
}
fn float_frame(data: &[u8]) -> (u32, Vec<f32>) {
let decoder = open(data, false);
next_page(&decoder);
let mut frame = next_frame(&decoder);
let samples =
unsafe { std::slice::from_raw_parts(frame.data, frame.length) }
.chunks_exact(4)
.map(|bytes| f32::from_ne_bytes(bytes.try_into().unwrap()))
.collect();
let format = frame.format as u32;
unsafe { dnrs_frame_clear(&mut frame) };
(format, samples)
}
#[test]
fn exr_and_radiance_keep_linear_floats() {
use image::{ExtendedColorType, ImageEncoder};
fn bytes(samples: &[f32]) -> Vec<u8> {
samples.iter().copied().flat_map(f32::to_ne_bytes).collect()
}
let mut data = Vec::new();
image::codecs::hdr::HdrEncoder::new(&mut data)
.write_image(&bytes(&[4.0, 0.5, 0.25]), 1, 1, ExtendedColorType::Rgb32F)
.unwrap();
assert_eq!(
float_frame(&data),
(dnrs_pixel_format::RgbF32 as u32, vec![4.0, 0.5, 0.25])
);
// Associated alpha comes out straight, negative values and all.
let mut exr = Cursor::new(Vec::new());
image::codecs::openexr::OpenExrEncoder::new(&mut exr)
.write_image(
&bytes(&[2.0, -0.5, 1.0, 0.5, 1.0, 1.0, 1.0, 0.0]),
2,
1,
ExtendedColorType::Rgba32F,
)
.unwrap();
assert_eq!(
float_frame(exr.get_ref()),
(
dnrs_pixel_format::RgbaF32 as u32,
vec![4.0, -1.0, 2.0, 0.5, 1.0, 1.0, 1.0, 0.0]
)
);
}
#[test]
fn dds_dxt1_is_decodable() {
fn push_u32(data: &mut Vec<u8>, value: u32) {
+42 -2
View File
@@ -49,6 +49,11 @@ int unpack_linear_output(int packed)
return (packed >> 20) & 1;
}
int unpack_hdr(int packed)
{
return (packed >> 21) & 1;
}
#ifndef DN_COMPUTE
// Squares of `size` device pixels, resolved from one design size by the
// caller, as the browser resolves its thumbnail checker size.
@@ -217,17 +222,52 @@ vec4 associated_to_working(vec4 t, int packed, bool opaque)
return associated_to_linear(t, unpack_transfer(packed), opaque);
}
#ifndef DN_COMPUTE
// ISO 21496-1 gain map, in the stored frame, at any resolution.
layout(set = 0, binding = 2) uniform sampler2D u_gain;
// Log2 gain at a source texel, sampled bilinearly in normalized source
// coordinates. `gain` is (scale, min, 1 / gamma, offset), weighted.
float gain_log2(ivec2 p, ivec2 image_size, vec4 gain)
{
ivec2 size = textureSize(u_gain, 0);
vec2 pos = (vec2(p) + 0.5) / vec2(image_size) * vec2(size) - 0.5;
ivec2 i0 = ivec2(floor(pos));
vec2 t = pos - vec2(i0);
ivec2 hi = size - 1;
float a = texelFetch(u_gain, clamp(i0, ivec2(0), hi), 0).r;
float b = texelFetch(u_gain, clamp(i0 + ivec2(1, 0), ivec2(0), hi), 0).r;
float c = texelFetch(u_gain, clamp(i0 + ivec2(0, 1), ivec2(0), hi), 0).r;
float d = texelFetch(u_gain, clamp(i0 + ivec2(1, 1), ivec2(0), hi), 0).r;
float g = mix(mix(a, b, t.x), mix(c, d, t.x), t.y);
return gain.y + gain.x * pow(g, gain.z);
}
// Applies the map to a linear, premultiplied working value, where the
// offset scales with alpha. Zero weight leaves the value untouched.
vec4 apply_gain(vec4 t, ivec2 p, ivec2 image_size, vec4 gain)
{
if (gain.x == 0.0 && gain.y == 0.0)
return t;
float k = gain.w * t.a;
return vec4((t.rgb + k) * exp2(gain_log2(p, image_size, gain)) - k, t.a);
}
#endif
#ifndef DN_COMPUTE
// Alpha is resolved here because the image sits on a known background.
// Filtering and composition retain their selected working space; output
// encoding only changes the representation emitted after resolving alpha.
vec4 finish_scale(vec4 premul, int transfer, bool checkerboard,
bool composite, bool linear_working, bool linear_output,
vec3 background, vec3 checker_background, float checker_size)
bool hdr, vec3 background, vec3 checker_background,
float checker_size)
{
float a = clamp(premul.a, 0.0, 1.0);
// Nohalo takes minmod slopes per channel, so RGB can outrun alpha.
vec3 rgb = clamp(premul.rgb, vec3(0.0), vec3(a));
// Only SDR white bounds it, though.
vec3 rgb = hdr ? max(premul.rgb, vec3(0.0))
: clamp(premul.rgb, vec3(0.0), vec3(a));
if (checkerboard)
background = checker(background, checker_background, checker_size);
else if (!composite) {
+14
View File
@@ -17,6 +17,11 @@ layout(push_constant) uniform Push {
float levels; // 0 disables dithering; otherwise 2^bpc - 1
uint premultiplied;
uint srgb_attachment;
// Extended presentation passes linear light on, in the platform's
// primaries, with SDR white at `white`.
uint extended;
vec4 matrix[3]; // Columns, from display linear RGB
float white;
} pc;
float
@@ -34,6 +39,15 @@ void
main()
{
vec4 c = texelFetch(u_compose, ivec2(gl_FragCoord.xy), 0);
if (pc.extended != 0) {
// Linear premultiplied values pass through the matrix as they are.
mat3 m = mat3(pc.matrix[0].xyz, pc.matrix[1].xyz, pc.matrix[2].xyz);
c.rgb = m * c.rgb * pc.white;
if (pc.premultiplied == 0)
c.rgb = c.a > 0.0 ? c.rgb / c.a : vec3(0.0);
out_color = c;
return;
}
c.rgb = c.a > 0.0 ? profile_curve(c.rgb / c.a, true) : vec3(0.0);
if (pc.premultiplied != 0)
c.rgb *= c.a;
+9 -7
View File
@@ -36,21 +36,22 @@ layout(push_constant) uniform Push {
float bg_r, bg_g, bg_b;
float checker_r, checker_g, checker_b;
float checker_size;
vec4 gain;
} pc;
// Callers clamp `p`, which the gain map takes too.
vec4 fetch_image(ivec2 p)
{
return texelFetch(u_tiles,
tile_coord(clamp(p, ivec2(0), pc.image_size - 1),
pc.image_size, pc.grid),
0);
return texelFetch(u_tiles, tile_coord(p, pc.image_size, pc.grid), 0);
}
vec4 fetch_working(ivec2 p, int orient, bool opaque)
{
return associated_to_working(
fetch_image(oriented_to_source(p, orient, pc.image_size)),
pc.transfer, opaque);
ivec2 src = clamp(oriented_to_source(p, orient, pc.image_size),
ivec2(0), pc.image_size - 1);
return apply_gain(
associated_to_working(fetch_image(src), pc.transfer, opaque), src,
pc.image_size, pc.gain);
}
#if DN_FILTER == DN_FILTER_NOHALO
@@ -188,6 +189,7 @@ void main()
unpack_composite(pc.transfer) != 0,
unpack_nonlinear(pc.transfer) == 0,
unpack_linear_output(pc.transfer) != 0,
unpack_hdr(pc.transfer) != 0,
vec3(pc.bg_r, pc.bg_g, pc.bg_b),
vec3(pc.checker_r, pc.checker_g, pc.checker_b),
pc.checker_size);
+13 -8
View File
@@ -31,14 +31,17 @@ layout(push_constant) uniform Push {
ivec2 mid_pad;
ivec2 layer_origin;
vec2 pan;
float angle; /* unused */
float bg_r, bg_g, bg_b; /* unused */
float checker_r, checker_g, checker_b; /* unused */
float checker_size; /* unused */
vec4 gain;
} pc;
// Callers clamp `p`, which the gain map takes too.
vec4 fetch_image(ivec2 p)
{
return texelFetch(u_tiles,
tile_coord(clamp(p, ivec2(0), pc.image_size - 1),
pc.image_size, pc.grid),
0);
return texelFetch(u_tiles, tile_coord(p, pc.image_size, pc.grid), 0);
}
void main()
@@ -70,10 +73,12 @@ void main()
float weight_sum = 0.0;
for (int x = first; x <= last; ++x) {
float w = filter_weight((pos_x - float(x)) * kernel_scale);
vec4 t = associated_to_working(
fetch_image(oriented_to_source(ivec2(x, row), orient,
pc.image_size)),
pc.transfer, opaque);
ivec2 src = clamp(oriented_to_source(ivec2(x, row), orient,
pc.image_size),
ivec2(0), pc.image_size - 1);
vec4 t = apply_gain(associated_to_working(fetch_image(src),
pc.transfer, opaque),
src, pc.image_size, pc.gain);
sum += t * w;
weight_sum += w;
}
+1
View File
@@ -77,6 +77,7 @@ void main()
unpack_composite(pc.transfer) != 0,
unpack_nonlinear(pc.transfer) == 0,
unpack_linear_output(pc.transfer) != 0,
unpack_hdr(pc.transfer) != 0,
vec3(pc.bg_r, pc.bg_g, pc.bg_b),
vec3(pc.checker_r, pc.checker_g, pc.checker_b),
pc.checker_size);
+20
View File
@@ -40,6 +40,13 @@ set(DAWN_TEST_FIXTURE_FILES
"${DAWN_TEST_FIXTURES_DIR}/chrm-p3-gama1.png"
"${DAWN_TEST_FIXTURES_DIR}/chrm-p3.png"
"${DAWN_TEST_FIXTURES_DIR}/display-p3-red_vs_srgb-red.png"
"${DAWN_TEST_FIXTURES_DIR}/gainmap.jpg"
"${DAWN_TEST_FIXTURES_DIR}/gainmap-colour.jpg"
"${DAWN_TEST_FIXTURES_DIR}/gainmap-flat.jpg"
"${DAWN_TEST_FIXTURES_DIR}/gainmap-iso.jpg"
"${DAWN_TEST_FIXTURES_DIR}/gainmap-map.jpg"
"${DAWN_TEST_FIXTURES_DIR}/gainmap-offsets.jpg"
"${DAWN_TEST_FIXTURES_DIR}/gainmap-seq.jpg"
"${DAWN_TEST_FIXTURES_DIR}/gama22.png"
"${DAWN_TEST_FIXTURES_DIR}/green.bmp"
"${DAWN_TEST_FIXTURES_DIR}/green.jpg"
@@ -113,6 +120,19 @@ target_compile_definitions(test-jpeg-crop PRIVATE
DAWN_TEST_FIXTURES_DIR="${DAWN_TEST_FIXTURES_DIR}")
add_test(NAME jpeg_crop COMMAND test-jpeg-crop)
add_executable(test-gain-map test-gain-map.cpp)
if (NOT CMAKE_CROSSCOMPILING)
add_dependencies(test-gain-map test-fixtures)
endif()
target_link_libraries(test-gain-map PRIVATE libdn)
target_compile_definitions(test-gain-map PRIVATE
DAWN_TEST_FIXTURES_DIR="${DAWN_TEST_FIXTURES_DIR}")
add_test(NAME gain_map COMMAND test-gain-map)
add_executable(test-split-hdr test-split-hdr.cpp)
target_link_libraries(test-split-hdr PRIVATE libdn)
add_test(NAME split_hdr COMMAND test-split-hdr)
add_executable(test-save test-save.cpp)
target_link_libraries(test-save PRIVATE libdn)
add_test(NAME save COMMAND test-save)
+114
View File
@@ -0,0 +1,114 @@
//
// exr.hpp: the least OpenEXR writer that loaders take
//
// Copyright The Dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#pragma once
#include <cstdint>
#include <cstring>
#include <span>
#include <string_view>
#include <vector>
namespace test
{
namespace exr_detail
{
inline void
append(std::vector<uint8_t> &o, const void *data, size_t size)
{
const auto *p = static_cast<const uint8_t *>(data);
o.insert(o.end(), p, p + size);
}
// OpenEXR is little-endian throughout, and so is every host Dawn runs on.
template <typename T>
inline void
append_le(std::vector<uint8_t> &o, T value)
{
append(o, &value, sizeof value);
}
inline void
append_attribute(std::vector<uint8_t> &o, std::string_view name,
std::string_view type, const std::vector<uint8_t> &value)
{
append(o, name.data(), name.size());
o.push_back(0);
append(o, type.data(), type.size());
o.push_back(0);
append_le(o, int32_t(value.size()));
append(o, value.data(), value.size());
}
} // namespace exr_detail
/// An uncompressed scanline file of FLOAT channels. `rgb` holds a triplet
/// per pixel, `alpha`, unless empty, a value, which OpenEXR takes for
/// associated.
inline std::vector<uint8_t>
write_exr(uint32_t width, uint32_t height, std::span<const float> rgb,
std::span<const float> alpha)
{
using namespace exr_detail;
// Channels go in alphabetical order, in the header and in each line.
std::vector<char> names = {'B', 'G', 'R'};
if (!alpha.empty())
names.insert(names.begin(), 'A');
std::vector<uint8_t> o = {0x76, 0x2f, 0x31, 0x01, 2, 0, 0, 0};
std::vector<uint8_t> value;
for (char name : names) {
value.push_back(uint8_t(name));
value.push_back(0);
append_le(value, int32_t(2)); // FLOAT
append_le(value, uint32_t(0)); // pLinear, reserved
append_le(value, int32_t(1));
append_le(value, int32_t(1));
}
value.push_back(0);
append_attribute(o, "channels", "chlist", value);
append_attribute(o, "compression", "compression", {0});
value.clear();
for (int32_t v : {0, 0, int32_t(width) - 1, int32_t(height) - 1})
append_le(value, v);
append_attribute(o, "dataWindow", "box2i", value);
append_attribute(o, "displayWindow", "box2i", value);
append_attribute(o, "lineOrder", "lineOrder", {0});
value.clear();
append_le(value, 1.f);
append_attribute(o, "pixelAspectRatio", "float", value);
append_attribute(o, "screenWindowWidth", "float", value);
value.clear();
append_le(value, 0.f);
append_le(value, 0.f);
append_attribute(o, "screenWindowCenter", "v2f", value);
o.push_back(0);
const size_t line = size_t(width) * names.size() * sizeof(float);
const size_t table = o.size();
for (uint32_t y = 0; y < height; y++)
append_le(o, uint64_t(table + height * 8 + y * (8 + line)));
for (uint32_t y = 0; y < height; y++) {
append_le(o, int32_t(y));
append_le(o, int32_t(line));
for (char name : names) {
const size_t channel = std::string_view("RGB").find(name);
for (uint32_t x = 0; x < width; x++) {
const size_t i = size_t(y) * width + x;
append_le(o, name == 'A' ? alpha[i] : rgb[i * 3 + channel]);
}
}
}
return o;
}
} // namespace test
+301 -3
View File
@@ -12,6 +12,8 @@
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>
@@ -742,9 +744,9 @@ write_svgs(const fs::path &dir)
}
static int
run_magick(initializer_list<const char *> args)
run_tool(const char *tool, initializer_list<const char *> args)
{
string cmd = "magick";
string cmd = tool;
for (const char *a : args) {
cmd += " '";
cmd += a;
@@ -752,11 +754,303 @@ run_magick(initializer_list<const char *> args)
}
int rc = system(cmd.c_str());
if (rc != 0)
fprintf(stderr, "gen_fixtures: warning: magick failed (%d): %s\n", rc,
fprintf(stderr, "gen_fixtures: warning: %s failed (%d): %s\n", tool, rc,
cmd.c_str());
return rc;
}
static int
run_magick(initializer_list<const char *> args)
{
return run_tool("magick", args);
}
// Tools that only some fixtures need, and whose absence skips their tests.
static bool
have_tool(const char *tool)
{
return !system((string("command -v ") + tool + " >/dev/null 2>&1").c_str());
}
// --- Gain maps ---------------------------------------------------------------
static vector<uint8_t>
read_all(const fs::path &path)
{
ifstream in(path, ios::binary);
return vector<uint8_t>(istreambuf_iterator<char>(in), {});
}
static void
append_be16(vector<uint8_t> &o, uint16_t v)
{
o.push_back(uint8_t(v >> 8));
o.push_back(uint8_t(v));
}
// A marker segment whose payload starts with a NUL-terminated identifier.
static void
append_jpeg_app(vector<uint8_t> &o, uint8_t marker, const string &id,
const vector<uint8_t> &payload)
{
o.push_back(0xFF);
o.push_back(marker);
append_be16(o, uint16_t(2 + id.size() + 1 + payload.size()));
o.insert(o.end(), id.begin(), id.end());
o.push_back(0);
o.insert(o.end(), payload.begin(), payload.end());
}
static vector<uint8_t>
bytes(const string &s)
{
return vector<uint8_t>(s.begin(), s.end());
}
static const string kXmpNs = "http://ns.adobe.com/xap/1.0/";
static const string kIsoNs = "urn:iso:std:iso:ts:21496:-1";
static string
xmp_packet(const string &description)
{
return "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\"><rdf:RDF xmlns:rdf="
"\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"><rdf:Description "
"rdf:about=\"\" "
"xmlns:hdrgm=\"http://ns.adobe.com/hdr-gain-map/1.0/\" "
"xmlns:Container=\"http://ns.google.com/photos/1.0/container/\" "
"xmlns:Item=\"http://ns.google.com/photos/1.0/container/item/\" " +
description + "</rdf:Description></rdf:RDF></x:xmpmeta>";
}
// ISO 21496-1 metadata for one channel: headrooms 0 and 2, the gain within
// [-0.5, 2], gamma 1.25, offsets 1/64--as the hdrgm XMP of write_gain_maps().
static vector<uint8_t>
iso_gain_map_metadata()
{
vector<uint8_t> o = {0, 0, 0, 0, 0};
for (uint32_t v :
{0u, 1u, 2u, 1u, uint32_t(-1), 2u, 2u, 1u, 5u, 4u, 1u, 64u, 1u, 64u})
append_be32(o, v);
return o;
}
/// An Ultra HDR JPEG: the base with a Multi-Picture Format index of two
/// images, then the map. Each image gets `*_app` segments right after SOI.
static void
write_mpf(const fs::path &path, const vector<uint8_t> &base,
const vector<uint8_t> &base_app, const vector<uint8_t> &map,
const vector<uint8_t> &map_app)
{
vector<uint8_t> second = {0xFF, 0xD8};
second.insert(second.end(), map_app.begin(), map_app.end());
second.insert(second.end(), map.begin() + 2, map.end());
// The MPF header starts after SOI, the application segments,
// APP2's marker and length, and the MPF identifier.
const size_t mpf_payload = 4 + 8 + 2 + 3 * 12 + 4 + 2 * 16;
const size_t origin = 2 + base_app.size() + 4 + 4;
const size_t first_size =
2 + base_app.size() + 4 + mpf_payload + (base.size() - 2);
vector<uint8_t> mpf = {'M', 'M', 0, 42};
append_be32(mpf, 8);
append_be16(mpf, 3);
append_be16(mpf, 0xB000); // MPFVersion
append_be16(mpf, 7);
append_be32(mpf, 4);