diff --git a/docs/source/about/usage.rst b/docs/source/about/usage.rst index 5a73851d..e48aa1fc 100644 --- a/docs/source/about/usage.rst +++ b/docs/source/about/usage.rst @@ -257,6 +257,18 @@ Considerations instead it simply starts a stream. - For the Linux flatpak you must prepend commands with ``flatpak-spawn --host``. +HDR Support +----------- +Streaming HDR content is supported for Windows hosts with NVIDIA, AMD, or Intel GPUs that support encoding HEVC Main 10. +You must have an HDR-capable display or EDID emulator dongle connected to your host PC to activate HDR in Windows. + +- Ensure you enable the HDR option in your Moonlight client settings, otherwise the stream will be SDR. +- A good HDR experience relies on proper HDR display calibration both in Windows and in game. HDR calibration can differ significantly between client and host displays. +- We recommend calibrating the display by streaming the Windows HDR Calibration app to your client device and saving an HDR calibration profile to use while streaming. +- You may also need to tune the brightness slider or HDR calibration options in game to the different HDR brightness capabilities of your client's display. +- Older games that use NVIDIA-specific NVAPI HDR rather than native Windows 10 OS HDR support may not display in HDR. +- Some GPUs can produce lower image quality or encoding performance when streaming in HDR compared to SDR. + Tutorials --------- Tutorial videos are available `here `_. diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h index a72c94bd..dac853fc 100644 --- a/src/platform/windows/display.h +++ b/src/platform/windows/display.h @@ -151,6 +151,7 @@ protected: } const char *dxgi_format_to_string(DXGI_FORMAT format); + const char *colorspace_to_string(DXGI_COLOR_SPACE_TYPE type); virtual capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) = 0; virtual int complete_img(img_t *img, bool dummy) = 0; diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 5706f86c..39be1112 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -563,6 +563,25 @@ int display_base_t::init(const ::video::config_t &config, const std::string &dis BOOST_LOG(info) << "Desktop resolution ["sv << dup_desc.ModeDesc.Width << 'x' << dup_desc.ModeDesc.Height << ']'; BOOST_LOG(info) << "Desktop format ["sv << dxgi_format_to_string(dup_desc.ModeDesc.Format) << ']'; + dxgi::output6_t output6 {}; + status = output->QueryInterface(IID_IDXGIOutput6, (void **)&output6); + if(SUCCEEDED(status)) { + DXGI_OUTPUT_DESC1 desc1; + output6->GetDesc1(&desc1); + + BOOST_LOG(info) + << std::endl + << "Colorspace : "sv << colorspace_to_string(desc1.ColorSpace) << std::endl + << "Bits Per Color : "sv << desc1.BitsPerColor << std::endl + << "Red Primary : ["sv << desc1.RedPrimary[0] << ',' << desc1.RedPrimary[1] << ']' << std::endl + << "Green Primary : ["sv << desc1.GreenPrimary[0] << ',' << desc1.GreenPrimary[1] << ']' << std::endl + << "Blue Primary : ["sv << desc1.BluePrimary[0] << ',' << desc1.BluePrimary[1] << ']' << std::endl + << "White Point : ["sv << desc1.WhitePoint[0] << ',' << desc1.WhitePoint[1] << ']' << std::endl + << "Min Luminance : "sv << desc1.MinLuminance << " nits"sv << std::endl + << "Max Luminance : "sv << desc1.MaxLuminance << " nits"sv << std::endl + << "Max Full Luminance : "sv << desc1.MaxFullFrameLuminance << " nits"sv; + } + // Capture format will be determined from the first call to AcquireNextFrame() capture_format = DXGI_FORMAT_UNKNOWN; @@ -598,6 +617,23 @@ bool display_base_t::get_hdr_metadata(SS_HDR_METADATA &metadata) { DXGI_OUTPUT_DESC1 desc1; output6->GetDesc1(&desc1); + // The primaries reported here seem to correspond to scRGB (Rec. 709) + // which we then convert to Rec 2020 in our scRGB FP16 -> PQ shader + // prior to encoding. It's not clear to me if we're supposed to report + // the primaries of the original colorspace or the one we've converted + // it to, but let's just report Rec 2020 primaries and D65 white level + // to avoid confusing clients by reporting Rec 709 primaries with a + // Rec 2020 colorspace. It seems like most clients ignore the primaries + // in the metadata anyway (luminance range is most important). + desc1.RedPrimary[0] = 0.708f; + desc1.RedPrimary[1] = 0.292f; + desc1.GreenPrimary[0] = 0.170f; + desc1.GreenPrimary[1] = 0.797f; + desc1.BluePrimary[0] = 0.131f; + desc1.BluePrimary[1] = 0.046f; + desc1.WhitePoint[0] = 0.3127f; + desc1.WhitePoint[1] = 0.3290f; + metadata.displayPrimaries[0].x = desc1.RedPrimary[0] * 50000; metadata.displayPrimaries[0].y = desc1.RedPrimary[1] * 50000; metadata.displayPrimaries[1].x = desc1.GreenPrimary[0] * 50000; @@ -749,6 +785,43 @@ const char *display_base_t::dxgi_format_to_string(DXGI_FORMAT format) { return format_str[format]; } +const char *display_base_t::colorspace_to_string(DXGI_COLOR_SPACE_TYPE type) { + const char *type_str[] = { + "DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709", + "DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709", + "DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P709", + "DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P2020", + "DXGI_COLOR_SPACE_RESERVED", + "DXGI_COLOR_SPACE_YCBCR_FULL_G22_NONE_P709_X601", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P601", + "DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P601", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709", + "DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P709", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P2020", + "DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P2020", + "DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_LEFT_P2020", + "DXGI_COLOR_SPACE_RGB_STUDIO_G2084_NONE_P2020", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_TOPLEFT_P2020", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_TOPLEFT_P2020", + "DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P2020", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_GHLG_TOPLEFT_P2020", + "DXGI_COLOR_SPACE_YCBCR_FULL_GHLG_TOPLEFT_P2020", + "DXGI_COLOR_SPACE_RGB_STUDIO_G24_NONE_P709", + "DXGI_COLOR_SPACE_RGB_STUDIO_G24_NONE_P2020", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_LEFT_P709", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_LEFT_P2020", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_TOPLEFT_P2020", + }; + + if(type < ARRAYSIZE(type_str)) { + return type_str[type]; + } + else { + return "UNKNOWN"; + } +} + } // namespace platf::dxgi namespace platf { diff --git a/src/platform/windows/display_vram.cpp b/src/platform/windows/display_vram.cpp index b50f2c24..ab63974b 100644 --- a/src/platform/windows/display_vram.cpp +++ b/src/platform/windows/display_vram.cpp @@ -89,9 +89,12 @@ blend_t make_blend(device_t::pointer device, bool enable, bool invert) { blob_t convert_UV_vs_hlsl; blob_t convert_UV_ps_hlsl; +blob_t convert_UV_PQ_ps_hlsl; blob_t scene_vs_hlsl; blob_t convert_Y_ps_hlsl; +blob_t convert_Y_PQ_ps_hlsl; blob_t scene_ps_hlsl; +blob_t scene_NW_ps_hlsl; struct img_d3d_t : public platf::img_t { std::shared_ptr display; @@ -546,28 +549,39 @@ public: return -1; } - status = device->CreatePixelShader(convert_Y_ps_hlsl->GetBufferPointer(), convert_Y_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_ps); - if(status) { - BOOST_LOG(error) << "Failed to create convertY pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - status = device->CreatePixelShader(convert_UV_ps_hlsl->GetBufferPointer(), convert_UV_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_ps); - if(status) { - BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - status = device->CreateVertexShader(convert_UV_vs_hlsl->GetBufferPointer(), convert_UV_vs_hlsl->GetBufferSize(), nullptr, &convert_UV_vs); if(status) { BOOST_LOG(error) << "Failed to create convertUV vertex shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } - status = device->CreatePixelShader(scene_ps_hlsl->GetBufferPointer(), scene_ps_hlsl->GetBufferSize(), nullptr, &scene_ps); - if(status) { - BOOST_LOG(error) << "Failed to create scene pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; + // If the display is in HDR and we're streaming HDR, we'll be converting scRGB to SMPTE 2084 PQ. + // NB: We can consume scRGB in SDR with our regular shaders because it behaves like UNORM input. + if(format == DXGI_FORMAT_P010 && display->is_hdr()) { + status = device->CreatePixelShader(convert_Y_PQ_ps_hlsl->GetBufferPointer(), convert_Y_PQ_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_ps); + if(status) { + BOOST_LOG(error) << "Failed to create convertY pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + status = device->CreatePixelShader(convert_UV_PQ_ps_hlsl->GetBufferPointer(), convert_UV_PQ_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_ps); + if(status) { + BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + } + else { + status = device->CreatePixelShader(convert_Y_ps_hlsl->GetBufferPointer(), convert_Y_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_ps); + if(status) { + BOOST_LOG(error) << "Failed to create convertY pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + status = device->CreatePixelShader(convert_UV_ps_hlsl->GetBufferPointer(), convert_UV_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_ps); + if(status) { + BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } } color_matrix = make_buffer(device.get(), ::video::colors[0]); @@ -708,7 +722,6 @@ public: vs_t convert_UV_vs; ps_t convert_UV_ps; ps_t convert_Y_ps; - ps_t scene_ps; vs_t scene_vs; D3D11_VIEWPORT outY_view; @@ -978,10 +991,32 @@ int display_vram_t::init(const ::video::config_t &config, const std::string &dis return -1; } - status = device->CreatePixelShader(scene_ps_hlsl->GetBufferPointer(), scene_ps_hlsl->GetBufferSize(), nullptr, &scene_ps); - if(status) { - BOOST_LOG(error) << "Failed to create scene pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; + if(config.dynamicRange && is_hdr()) { + // This shader will normalize scRGB white levels to a user-defined white level + status = device->CreatePixelShader(scene_NW_ps_hlsl->GetBufferPointer(), scene_NW_ps_hlsl->GetBufferSize(), nullptr, &scene_ps); + if(status) { + BOOST_LOG(error) << "Failed to create scene pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + // Use a 300 nit target for the mouse cursor. We should really get + // the user's SDR white level in nits, but there is no API that + // provides that information to Win32 apps. + float sdr_multiplier_data[16 / sizeof(float)] { 300.0f / 80.f }; // aligned to 16-byte + auto sdr_multiplier = make_buffer(device.get(), sdr_multiplier_data); + if(!sdr_multiplier) { + BOOST_LOG(warning) << "Failed to create SDR multiplier"sv; + return -1; + } + + device_ctx->PSSetConstantBuffers(0, 1, &sdr_multiplier); + } + else { + status = device->CreatePixelShader(scene_ps_hlsl->GetBufferPointer(), scene_ps_hlsl->GetBufferSize(), nullptr, &scene_ps); + if(status) { + BOOST_LOG(error) << "Failed to create scene pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } } blend_alpha = make_blend(device.get(), true, false); @@ -1108,7 +1143,25 @@ std::vector display_vram_t::get_supported_sdr_capture_formats() { } std::vector display_vram_t::get_supported_hdr_capture_formats() { - return { DXGI_FORMAT_R10G10B10A2_UNORM }; + return { + // scRGB FP16 is the desired format for HDR content. This will also handle + // 10-bit SDR displays with the increased precision of FP16 vs 8-bit UNORMs. + DXGI_FORMAT_R16G16B16A16_FLOAT, + + // DXGI_FORMAT_R10G10B10A2_UNORM seems like it might give us frames already + // converted to SMPTE 2084 PQ, however it seems to actually just clamp the + // scRGB FP16 values that DWM is using when the desktop format is scRGB FP16. + // + // If there is a case where the desktop format is really SMPTE 2084 PQ, it + // might make sense to support capturing it without conversion to scRGB, + // but we avoid it for now. + + // We include the 8-bit modes too for when the display is in SDR mode, + // while the client stream is HDR-capable. These UNORM formats behave + // like a degenerate case of scRGB FP16 with values between 0.0f-1.0f. + DXGI_FORMAT_B8G8R8A8_UNORM, + DXGI_FORMAT_R8G8B8A8_UNORM, + }; } std::shared_ptr display_vram_t::make_hwdevice(pix_fmt_e pix_fmt) { @@ -1144,11 +1197,21 @@ int init() { return -1; } + convert_Y_PQ_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertYPS_PQ.hlsl"); + if(!convert_Y_PQ_ps_hlsl) { + return -1; + } + convert_UV_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertUVPS.hlsl"); if(!convert_UV_ps_hlsl) { return -1; } + convert_UV_PQ_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertUVPS_PQ.hlsl"); + if(!convert_UV_PQ_ps_hlsl) { + return -1; + } + convert_UV_vs_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/ConvertUVVS.hlsl"); if(!convert_UV_vs_hlsl) { return -1; @@ -1158,6 +1221,11 @@ int init() { if(!scene_ps_hlsl) { return -1; } + + scene_NW_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ScenePS_NW.hlsl"); + if(!scene_NW_ps_hlsl) { + return -1; + } BOOST_LOG(info) << "Compiled shaders"sv; return 0; diff --git a/src/stream.cpp b/src/stream.cpp index 78e7a3d0..c78fbbd2 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -820,8 +820,11 @@ void controlBroadcastThread(control_server_t *server) { send_rumble(session, rumble->id, rumble->lowfreq, rumble->highfreq); } + // Unlike rumble which we send as best-effort, HDR state messages are critical + // for proper functioning of some clients. We must wait to pop entries from + // the queue until we're sure we have a peer to send them to. auto &hdr_queue = session->control.hdr_queue; - while(hdr_queue->peek()) { + while(session->control.peer && hdr_queue->peek()) { auto hdr_info = hdr_queue->pop(); send_hdr_mode(session, std::move(hdr_info)); diff --git a/src/video.cpp b/src/video.cpp index 91f9bfba..8fddb8f4 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -1002,35 +1002,46 @@ std::optional make_session(platf::display_t *disp, const encoder_t &e ctx->color_range = (config.encoderCscMode & 0x1) ? AVCOL_RANGE_JPEG : AVCOL_RANGE_MPEG; int sws_color_space; - switch(config.encoderCscMode >> 1) { - case 0: - default: - // Rec. 601 - BOOST_LOG(info) << "Color coding [Rec. 601]"sv; - ctx->color_primaries = AVCOL_PRI_SMPTE170M; - ctx->color_trc = AVCOL_TRC_SMPTE170M; - ctx->colorspace = AVCOL_SPC_SMPTE170M; - sws_color_space = SWS_CS_SMPTE170M; - break; - - case 1: - // Rec. 709 - BOOST_LOG(info) << "Color coding [Rec. 709]"sv; - ctx->color_primaries = AVCOL_PRI_BT709; - ctx->color_trc = AVCOL_TRC_BT709; - ctx->colorspace = AVCOL_SPC_BT709; - sws_color_space = SWS_CS_ITU709; - break; - - case 2: - // Rec. 2020 - BOOST_LOG(info) << "Color coding [Rec. 2020]"sv; + if(config.dynamicRange && disp->is_hdr()) { + // When HDR is active, that overrides the colorspace the client requested + BOOST_LOG(info) << "HDR color coding [Rec. 2020 + SMPTE 2084 PQ]"sv; ctx->color_primaries = AVCOL_PRI_BT2020; - ctx->color_trc = AVCOL_TRC_BT2020_10; + ctx->color_trc = AVCOL_TRC_SMPTE2084; ctx->colorspace = AVCOL_SPC_BT2020_NCL; sws_color_space = SWS_CS_BT2020; - break; } + else { + switch(config.encoderCscMode >> 1) { + case 0: + default: + // Rec. 601 + BOOST_LOG(info) << "SDR color coding [Rec. 601]"sv; + ctx->color_primaries = AVCOL_PRI_SMPTE170M; + ctx->color_trc = AVCOL_TRC_SMPTE170M; + ctx->colorspace = AVCOL_SPC_SMPTE170M; + sws_color_space = SWS_CS_SMPTE170M; + break; + + case 1: + // Rec. 709 + BOOST_LOG(info) << "SDR color coding [Rec. 709]"sv; + ctx->color_primaries = AVCOL_PRI_BT709; + ctx->color_trc = AVCOL_TRC_BT709; + ctx->colorspace = AVCOL_SPC_BT709; + sws_color_space = SWS_CS_ITU709; + break; + + case 2: + // Rec. 2020 + BOOST_LOG(info) << "SDR color coding [Rec. 2020]"sv; + ctx->color_primaries = AVCOL_PRI_BT2020; + ctx->color_trc = AVCOL_TRC_BT2020_10; + ctx->colorspace = AVCOL_SPC_BT2020_NCL; + sws_color_space = SWS_CS_BT2020; + break; + } + } + BOOST_LOG(info) << "Color range: ["sv << ((config.encoderCscMode & 0x1) ? "JPEG"sv : "MPEG"sv) << ']'; AVPixelFormat sw_fmt; @@ -1039,15 +1050,6 @@ std::optional make_session(platf::display_t *disp, const encoder_t &e } else { sw_fmt = encoder.dynamic_pix_fmt; - - // When HDR is active, that overrides the colorspace the client requested - if(disp->is_hdr()) { - BOOST_LOG(info) << "HDR color coding override [SMPTE ST 2084 PQ]"sv; - ctx->color_primaries = AVCOL_PRI_BT2020; - ctx->color_trc = AVCOL_TRC_SMPTE2084; - ctx->colorspace = AVCOL_SPC_BT2020_NCL; - sws_color_space = SWS_CS_BT2020; - } } // Used by cbs::make_sps_hevc diff --git a/src_assets/windows/assets/shaders/directx/ConvertUVPS_PQ.hlsl b/src_assets/windows/assets/shaders/directx/ConvertUVPS_PQ.hlsl new file mode 100644 index 00000000..3ae0fb1d --- /dev/null +++ b/src_assets/windows/assets/shaders/directx/ConvertUVPS_PQ.hlsl @@ -0,0 +1,69 @@ +Texture2D image : register(t0); + +SamplerState def_sampler : register(s0); + +struct FragTexWide { + float3 uuv : TEXCOORD0; +}; + +cbuffer ColorMatrix : register(b0) { + float4 color_vec_y; + float4 color_vec_u; + float4 color_vec_v; + float2 range_y; + float2 range_uv; +}; + +float3 NitsToPQ(float3 L) +{ + // Constants from SMPTE 2084 PQ + static const float m1 = 2610.0 / 4096.0 / 4; + static const float m2 = 2523.0 / 4096.0 * 128; + static const float c1 = 3424.0 / 4096.0; + static const float c2 = 2413.0 / 4096.0 * 32; + static const float c3 = 2392.0 / 4096.0 * 32; + + float3 Lp = pow(saturate(L / 10000.0), m1); + return pow((c1 + c2 * Lp) / (1 + c3 * Lp), m2); +} + +float3 Rec709toRec2020(float3 rec709) +{ + static const float3x3 ConvMat = + { + 0.627402, 0.329292, 0.043306, + 0.069095, 0.919544, 0.011360, + 0.016394, 0.088028, 0.895578 + }; + return mul(ConvMat, rec709); +} + +float3 scRGBTo2100PQ(float3 rgb) +{ + // Convert from Rec 709 primaries (used by scRGB) to Rec 2020 primaries (used by Rec 2100) + rgb = Rec709toRec2020(rgb); + + // 1.0f is defined as 80 nits in the scRGB colorspace + rgb *= 80; + + // Apply the PQ transfer function on the raw color values in nits + return NitsToPQ(rgb); +} + +//-------------------------------------------------------------------------------------- +// Pixel Shader +//-------------------------------------------------------------------------------------- +float2 main_ps(FragTexWide input) : SV_Target +{ + float3 rgb_left = scRGBTo2100PQ(image.Sample(def_sampler, input.uuv.xz).rgb); + float3 rgb_right = scRGBTo2100PQ(image.Sample(def_sampler, input.uuv.yz).rgb); + float3 rgb = (rgb_left + rgb_right) * 0.5; + + float u = dot(color_vec_u.xyz, rgb) + color_vec_u.w; + float v = dot(color_vec_v.xyz, rgb) + color_vec_v.w; + + u = u * range_uv.x + range_uv.y; + v = v * range_uv.x + range_uv.y; + + return float2(u, v); +} diff --git a/src_assets/windows/assets/shaders/directx/ConvertYPS_PQ.hlsl b/src_assets/windows/assets/shaders/directx/ConvertYPS_PQ.hlsl new file mode 100644 index 00000000..cc3054e8 --- /dev/null +++ b/src_assets/windows/assets/shaders/directx/ConvertYPS_PQ.hlsl @@ -0,0 +1,62 @@ +Texture2D image : register(t0); + +SamplerState def_sampler : register(s0); + +cbuffer ColorMatrix : register(b0) { + float4 color_vec_y; + float4 color_vec_u; + float4 color_vec_v; + float2 range_y; + float2 range_uv; +}; + +struct PS_INPUT +{ + float4 pos : SV_POSITION; + float2 tex : TEXCOORD; +}; + +float3 NitsToPQ(float3 L) +{ + // Constants from SMPTE 2084 PQ + static const float m1 = 2610.0 / 4096.0 / 4; + static const float m2 = 2523.0 / 4096.0 * 128; + static const float c1 = 3424.0 / 4096.0; + static const float c2 = 2413.0 / 4096.0 * 32; + static const float c3 = 2392.0 / 4096.0 * 32; + + float3 Lp = pow(saturate(L / 10000.0), m1); + return pow((c1 + c2 * Lp) / (1 + c3 * Lp), m2); +} + +float3 Rec709toRec2020(float3 rec709) +{ + static const float3x3 ConvMat = + { + 0.627402, 0.329292, 0.043306, + 0.069095, 0.919544, 0.011360, + 0.016394, 0.088028, 0.895578 + }; + return mul(ConvMat, rec709); +} + +float3 scRGBTo2100PQ(float3 rgb) +{ + // Convert from Rec 709 primaries (used by scRGB) to Rec 2020 primaries (used by Rec 2100) + rgb = Rec709toRec2020(rgb); + + // 1.0f is defined as 80 nits in the scRGB colorspace + rgb *= 80; + + // Apply the PQ transfer function on the raw color values in nits + return NitsToPQ(rgb); +} + +float main_ps(PS_INPUT frag_in) : SV_Target +{ + float3 rgb = scRGBTo2100PQ(image.Sample(def_sampler, frag_in.tex, 0).rgb); + + float y = dot(color_vec_y.xyz, rgb) + color_vec_y.w; + + return y * range_y.x + range_y.y; +} diff --git a/src_assets/windows/assets/shaders/directx/ScenePS_NW.hlsl b/src_assets/windows/assets/shaders/directx/ScenePS_NW.hlsl new file mode 100644 index 00000000..9088b767 --- /dev/null +++ b/src_assets/windows/assets/shaders/directx/ScenePS_NW.hlsl @@ -0,0 +1,22 @@ +Texture2D image : register(t0); + +SamplerState def_sampler : register(s0); + +struct PS_INPUT +{ + float4 pos : SV_POSITION; + float2 tex : TEXCOORD; +}; + +cbuffer SdrScaling : register(b0) { + float scale_factor; +}; + +float4 main_ps(PS_INPUT frag_in) : SV_Target +{ + float4 rgba = image.Sample(def_sampler, frag_in.tex, 0); + + rgba.rgb = rgba.rgb * scale_factor; + + return rgba; +}