HDR loading and display
This is way too complicated. Windows not tested.
This commit is contained in:
+1
-1
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -97,6 +97,7 @@ enum class Action : uint8_t {
|
||||
Lock,
|
||||
Fixate,
|
||||
ColorManagement,
|
||||
HighDynamicRange,
|
||||
Smooth,
|
||||
Checkerboard,
|
||||
NonlinearProcessing,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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_};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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_); }
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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, ©);
|
||||
},
|
||||
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, ©);
|
||||
},
|
||||
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, ©);
|
||||
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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||