Implement perceptual gamma / contrast correction (#37167)

Closes #36023 

This improves font rendering quality by doing perceptual gamma+contrast
correction which makes font edges look nicer and more legible.

A comparison image: (left is old, right is new)
<img width="1638" height="854" alt="Screenshot 2025-08-29 140015"
src="https://github.com/user-attachments/assets/85ca9818-0d55-4af0-a796-19e8cf9ed36b"
/>

This is most noticeable on smaller fonts / low-dpi displays

Release Notes:

- Improved font rendering quality
This commit is contained in:
localcc 2025-09-01 20:07:45 +02:00 committed by GitHub
parent 61175ab9cd
commit d910feac1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 157 additions and 127 deletions

View file

@ -696,6 +696,7 @@ features = [
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_Graphics_Hlsl",
"Win32_Networking_WinSock",
"Win32_Security",
"Win32_Security_Credentials",

View file

@ -0,0 +1,28 @@
float color_brightness(float3 color) {
// REC. 601 luminance coefficients for percieved brightness
return dot(color, float3(0.30f, 0.59f, 0.11f));
}
float light_on_dark_contrast(float enhancedContrast, float3 color) {
float brightness = color_brightness(color);
float multiplier = saturate(4.0f * (0.75f - brightness));
return enhancedContrast * multiplier;
}
float enhance_contrast(float alpha, float k) {
return alpha * (k + 1.0f) / (alpha * k + 1.0f);
}
float apply_alpha_correction(float a, float b, float4 g) {
float brightness_adjustment = g.x * b + g.y;
float correction = brightness_adjustment * a + (g.z * b + g.w);
return a + a * (1.0f - a) * correction;
}
float apply_contrast_and_gamma_correction(float sample, float3 color, float enhanced_contrast_factor, float4 gamma_ratios) {
float enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color);
float brightness = color_brightness(color);
float contrasted = enhance_contrast(sample, enhanced_contrast);
return apply_alpha_correction(contrasted, brightness, gamma_ratios);
}

View file

@ -1,3 +1,5 @@
#include "alpha_correction.hlsl"
struct RasterVertexOutput {
float4 position : SV_Position;
float2 texcoord : TEXCOORD0;
@ -23,17 +25,19 @@ struct Bounds {
int2 size;
};
Texture2D<float4> t_layer : register(t0);
Texture2D<float> t_layer : register(t0);
SamplerState s_layer : register(s0);
cbuffer GlyphLayerTextureParams : register(b0) {
Bounds bounds;
float4 run_color;
float4 gamma_ratios;
float grayscale_enhanced_contrast;
float3 _pad;
};
float4 emoji_rasterization_fragment(PixelInput input): SV_Target {
float3 sampled = t_layer.Sample(s_layer, input.texcoord.xy).rgb;
float alpha = (sampled.r + sampled.g + sampled.b) / 3;
return float4(run_color.rgb, alpha);
float sample = t_layer.Sample(s_layer, input.texcoord.xy).r;
float alpha_corrected = apply_contrast_and_gamma_correction(sample, run_color.rgb, grayscale_enhanced_contrast, gamma_ratios);
return float4(run_color.rgb, alpha_corrected * run_color.a);
}

View file

@ -10,12 +10,8 @@ use windows::{
Foundation::*,
Globalization::GetUserDefaultLocaleName,
Graphics::{
Direct3D::D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP,
Direct3D11::*,
DirectWrite::*,
Dxgi::Common::*,
Gdi::{IsRectEmpty, LOGFONTW},
Imaging::*,
Direct3D::D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, Direct3D11::*, DirectWrite::*,
Dxgi::Common::*, Gdi::LOGFONTW,
},
System::SystemServices::LOCALE_NAME_MAX_LENGTH,
UI::WindowsAndMessaging::*,
@ -40,12 +36,10 @@ pub(crate) struct DirectWriteTextSystem(RwLock<DirectWriteState>);
struct DirectWriteComponent {
locale: String,
factory: IDWriteFactory5,
bitmap_factory: AgileReference<IWICImagingFactory>,
in_memory_loader: IDWriteInMemoryFontFileLoader,
builder: IDWriteFontSetBuilder1,
text_renderer: Arc<TextRendererWrapper>,
render_params: IDWriteRenderingParams3,
gpu_state: GPUState,
}
@ -76,11 +70,10 @@ struct FontIdentifier {
}
impl DirectWriteComponent {
pub fn new(bitmap_factory: &IWICImagingFactory, gpu_context: &DirectXDevices) -> Result<Self> {
pub fn new(gpu_context: &DirectXDevices) -> Result<Self> {
// todo: ideally this would not be a large unsafe block but smaller isolated ones for easier auditing
unsafe {
let factory: IDWriteFactory5 = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)?;
let bitmap_factory = AgileReference::new(bitmap_factory)?;
// The `IDWriteInMemoryFontFileLoader` here is supported starting from
// Windows 10 Creators Update, which consequently requires the entire
// `DirectWriteTextSystem` to run on `win10 1703`+.
@ -92,36 +85,14 @@ impl DirectWriteComponent {
let locale = String::from_utf16_lossy(&locale_vec);
let text_renderer = Arc::new(TextRendererWrapper::new(&locale));
let render_params = {
let default_params: IDWriteRenderingParams3 =
factory.CreateRenderingParams()?.cast()?;
let gamma = default_params.GetGamma();
let enhanced_contrast = default_params.GetEnhancedContrast();
let gray_contrast = default_params.GetGrayscaleEnhancedContrast();
let cleartype_level = default_params.GetClearTypeLevel();
let grid_fit_mode = default_params.GetGridFitMode();
factory.CreateCustomRenderingParams(
gamma,
enhanced_contrast,
gray_contrast,
cleartype_level,
DWRITE_PIXEL_GEOMETRY_RGB,
DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC,
grid_fit_mode,
)?
};
let gpu_state = GPUState::new(gpu_context)?;
Ok(DirectWriteComponent {
locale,
factory,
bitmap_factory,
in_memory_loader,
builder,
text_renderer,
render_params,
gpu_state,
})
}
@ -212,11 +183,8 @@ impl GPUState {
}
impl DirectWriteTextSystem {
pub(crate) fn new(
gpu_context: &DirectXDevices,
bitmap_factory: &IWICImagingFactory,
) -> Result<Self> {
let components = DirectWriteComponent::new(bitmap_factory, gpu_context)?;
pub(crate) fn new(gpu_context: &DirectXDevices) -> Result<Self> {
let components = DirectWriteComponent::new(gpu_context)?;
let system_font_collection = unsafe {
let mut result = std::mem::zeroed();
components
@ -762,14 +730,14 @@ impl DirectWriteState {
unsafe {
font.font_face.GetRecommendedRenderingMode(
params.font_size.0,
// The dpi here seems that it has the same effect with `Some(&transform)`
1.0,
1.0,
// Using 96 as scale is applied by the transform
96.0,
96.0,
Some(&transform),
false,
DWRITE_OUTLINE_THRESHOLD_ANTIALIASED,
DWRITE_MEASURING_MODE_NATURAL,
&self.components.render_params,
None,
&mut rendering_mode,
&mut grid_fit_mode,
)?;
@ -782,8 +750,7 @@ impl DirectWriteState {
rendering_mode,
DWRITE_MEASURING_MODE_NATURAL,
grid_fit_mode,
// We're using cleartype not grayscale for monochrome is because it provides better quality
DWRITE_TEXT_ANTIALIAS_MODE_CLEARTYPE,
DWRITE_TEXT_ANTIALIAS_MODE_GRAYSCALE,
baseline_origin_x,
baseline_origin_y,
)
@ -794,10 +761,14 @@ impl DirectWriteState {
fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
let glyph_analysis = self.create_glyph_run_analysis(params)?;
let bounds = unsafe { glyph_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_CLEARTYPE_3x1)? };
// Some glyphs cannot be drawn with ClearType, such as bitmap fonts. In that case
// GetAlphaTextureBounds() supposedly returns an empty RECT, but I haven't tested that yet.
if !unsafe { IsRectEmpty(&bounds) }.as_bool() {
let bounds = unsafe { glyph_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1)? };
if bounds.right < bounds.left {
Ok(Bounds {
origin: point(0.into(), 0.into()),
size: size(0.into(), 0.into()),
})
} else {
Ok(Bounds {
origin: point(bounds.left.into(), bounds.top.into()),
size: size(
@ -805,25 +776,6 @@ impl DirectWriteState {
(bounds.bottom - bounds.top).into(),
),
})
} else {
// If it's empty, retry with grayscale AA.
let bounds =
unsafe { glyph_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1)? };
if bounds.right < bounds.left {
Ok(Bounds {
origin: point(0.into(), 0.into()),
size: size(0.into(), 0.into()),
})
} else {
Ok(Bounds {
origin: point(bounds.left.into(), bounds.top.into()),
size: size(
(bounds.right - bounds.left).into(),
(bounds.bottom - bounds.top).into(),
),
})
}
}
}
@ -872,13 +824,12 @@ impl DirectWriteState {
glyph_bounds: Bounds<DevicePixels>,
) -> Result<Vec<u8>> {
let mut bitmap_data =
vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize * 3];
vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize];
let glyph_analysis = self.create_glyph_run_analysis(params)?;
unsafe {
glyph_analysis.CreateAlphaTexture(
// We're using cleartype not grayscale for monochrome is because it provides better quality
DWRITE_TEXTURE_CLEARTYPE_3x1,
DWRITE_TEXTURE_ALIASED_1x1,
&RECT {
left: glyph_bounds.origin.x.0,
top: glyph_bounds.origin.y.0,
@ -889,30 +840,6 @@ impl DirectWriteState {
)?;
}
let bitmap_factory = self.components.bitmap_factory.resolve()?;
let bitmap = unsafe {
bitmap_factory.CreateBitmapFromMemory(
glyph_bounds.size.width.0 as u32,
glyph_bounds.size.height.0 as u32,
&GUID_WICPixelFormat24bppRGB,
glyph_bounds.size.width.0 as u32 * 3,
&bitmap_data,
)
}?;
let grayscale_bitmap =
unsafe { WICConvertBitmapSource(&GUID_WICPixelFormat8bppGray, &bitmap) }?;
let mut bitmap_data =
vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize];
unsafe {
grayscale_bitmap.CopyPixels(
std::ptr::null() as _,
glyph_bounds.size.width.0 as u32,
&mut bitmap_data,
)
}?;
Ok(bitmap_data)
}
@ -981,25 +908,24 @@ impl DirectWriteState {
DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC,
DWRITE_MEASURING_MODE_NATURAL,
DWRITE_GRID_FIT_MODE_DEFAULT,
DWRITE_TEXT_ANTIALIAS_MODE_CLEARTYPE,
DWRITE_TEXT_ANTIALIAS_MODE_GRAYSCALE,
baseline_origin_x,
baseline_origin_y,
)
}?;
let color_bounds =
unsafe { color_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_CLEARTYPE_3x1) }?;
unsafe { color_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1) }?;
let color_size = size(
color_bounds.right - color_bounds.left,
color_bounds.bottom - color_bounds.top,
);
if color_size.width > 0 && color_size.height > 0 {
let mut alpha_data =
vec![0u8; (color_size.width * color_size.height * 3) as usize];
let mut alpha_data = vec![0u8; (color_size.width * color_size.height) as usize];
unsafe {
color_analysis.CreateAlphaTexture(
DWRITE_TEXTURE_CLEARTYPE_3x1,
DWRITE_TEXTURE_ALIASED_1x1,
&color_bounds,
&mut alpha_data,
)
@ -1015,10 +941,6 @@ impl DirectWriteState {
}
};
let bounds = bounds(point(color_bounds.left, color_bounds.top), color_size);
let alpha_data = alpha_data
.chunks_exact(3)
.flat_map(|chunk| [chunk[0], chunk[1], chunk[2], 255])
.collect::<Vec<_>>();
glyph_layers.push(GlyphLayerTexture::new(
&self.components.gpu_state,
run_color,
@ -1135,10 +1057,18 @@ impl DirectWriteState {
unsafe { device_context.PSSetSamplers(0, Some(&gpu_state.sampler)) };
unsafe { device_context.OMSetBlendState(&gpu_state.blend_state, None, 0xffffffff) };
let crate::FontInfo {
gamma_ratios,
grayscale_enhanced_contrast,
} = DirectXRenderer::get_font_info();
for layer in glyph_layers {
let params = GlyphLayerTextureParams {
run_color: layer.run_color,
bounds: layer.bounds,
gamma_ratios: *gamma_ratios,
grayscale_enhanced_contrast: *grayscale_enhanced_contrast,
_pad: [0f32; 3],
};
unsafe {
let mut dest = std::mem::zeroed();
@ -1298,7 +1228,7 @@ impl GlyphLayerTexture {
Height: texture_size.height as u32,
MipLevels: 1,
ArraySize: 1,
Format: DXGI_FORMAT_R8G8B8A8_UNORM,
Format: DXGI_FORMAT_R8_UNORM,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
@ -1334,7 +1264,7 @@ impl GlyphLayerTexture {
0,
None,
alpha_data.as_ptr() as _,
(texture_size.width * 4) as u32,
texture_size.width as u32,
0,
)
};
@ -1352,6 +1282,9 @@ impl GlyphLayerTexture {
struct GlyphLayerTextureParams {
bounds: Bounds<i32>,
run_color: Rgba,
gamma_ratios: [f32; 4],
grayscale_enhanced_contrast: f32,
_pad: [f32; 3],
}
struct TextRendererWrapper(pub IDWriteTextRenderer);

View file

@ -1,4 +1,7 @@
use std::{mem::ManuallyDrop, sync::Arc};
use std::{
mem::ManuallyDrop,
sync::{Arc, OnceLock},
};
use ::util::ResultExt;
use anyhow::{Context, Result};
@ -9,6 +12,7 @@ use windows::{
Direct3D::*,
Direct3D11::*,
DirectComposition::*,
DirectWrite::*,
Dxgi::{Common::*, *},
},
},
@ -27,6 +31,11 @@ const RENDER_TARGET_FORMAT: DXGI_FORMAT = DXGI_FORMAT_B8G8R8A8_UNORM;
// This configuration is used for MSAA rendering on paths only, and it's guaranteed to be supported by DirectX 11.
const PATH_MULTISAMPLE_COUNT: u32 = 4;
pub(crate) struct FontInfo {
pub gamma_ratios: [f32; 4],
pub grayscale_enhanced_contrast: f32,
}
pub(crate) struct DirectXRenderer {
hwnd: HWND,
atlas: Arc<DirectXAtlas>,
@ -35,6 +44,7 @@ pub(crate) struct DirectXRenderer {
globals: DirectXGlobalElements,
pipelines: DirectXRenderPipelines,
direct_composition: Option<DirectComposition>,
font_info: &'static FontInfo,
}
/// Direct3D objects
@ -171,6 +181,7 @@ impl DirectXRenderer {
globals,
pipelines,
direct_composition,
font_info: Self::get_font_info(),
})
}
@ -183,10 +194,12 @@ impl DirectXRenderer {
&self.devices.device_context,
self.globals.global_params_buffer[0].as_ref().unwrap(),
&[GlobalParams {
gamma_ratios: self.font_info.gamma_ratios,
viewport_size: [
self.resources.viewport[0].Width,
self.resources.viewport[0].Height,
],
grayscale_enhanced_contrast: self.font_info.grayscale_enhanced_contrast,
_pad: 0,
}],
)?;
@ -617,6 +630,52 @@ impl DirectXRenderer {
driver_info: driver_version,
})
}
pub(crate) fn get_font_info() -> &'static FontInfo {
static CACHED_FONT_INFO: OnceLock<FontInfo> = OnceLock::new();
CACHED_FONT_INFO.get_or_init(|| unsafe {
let factory: IDWriteFactory5 = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED).unwrap();
let render_params: IDWriteRenderingParams1 =
factory.CreateRenderingParams().unwrap().cast().unwrap();
FontInfo {
gamma_ratios: Self::get_gamma_ratios(render_params.GetGamma()),
grayscale_enhanced_contrast: render_params.GetGrayscaleEnhancedContrast(),
}
})
}
// Gamma ratios for brightening/darkening edges for better contrast
// https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.cpp#L50
fn get_gamma_ratios(gamma: f32) -> [f32; 4] {
const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
[0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0], // gamma = 1.0
[0.0166 / 4.0, -0.0807 / 4.0, 0.2227 / 4.0, -0.0751 / 4.0], // gamma = 1.1
[0.0350 / 4.0, -0.1760 / 4.0, 0.4325 / 4.0, -0.1370 / 4.0], // gamma = 1.2
[0.0543 / 4.0, -0.2821 / 4.0, 0.6302 / 4.0, -0.1876 / 4.0], // gamma = 1.3
[0.0739 / 4.0, -0.3963 / 4.0, 0.8167 / 4.0, -0.2287 / 4.0], // gamma = 1.4
[0.0933 / 4.0, -0.5161 / 4.0, 0.9926 / 4.0, -0.2616 / 4.0], // gamma = 1.5
[0.1121 / 4.0, -0.6395 / 4.0, 1.1588 / 4.0, -0.2877 / 4.0], // gamma = 1.6
[0.1300 / 4.0, -0.7649 / 4.0, 1.3159 / 4.0, -0.3080 / 4.0], // gamma = 1.7
[0.1469 / 4.0, -0.8911 / 4.0, 1.4644 / 4.0, -0.3234 / 4.0], // gamma = 1.8
[0.1627 / 4.0, -1.0170 / 4.0, 1.6051 / 4.0, -0.3347 / 4.0], // gamma = 1.9
[0.1773 / 4.0, -1.1420 / 4.0, 1.7385 / 4.0, -0.3426 / 4.0], // gamma = 2.0
[0.1908 / 4.0, -1.2652 / 4.0, 1.8650 / 4.0, -0.3476 / 4.0], // gamma = 2.1
[0.2031 / 4.0, -1.3864 / 4.0, 1.9851 / 4.0, -0.3501 / 4.0], // gamma = 2.2
];
const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
[
ratios[0] * NORM13,
ratios[1] * NORM24,
ratios[2] * NORM13,
ratios[3] * NORM24,
]
}
}
impl DirectXResources {
@ -822,8 +881,10 @@ impl DirectXGlobalElements {
#[derive(Debug, Default)]
#[repr(C)]
struct GlobalParams {
gamma_ratios: [f32; 4],
viewport_size: [f32; 2],
_pad: u64,
grayscale_enhanced_contrast: f32,
_pad: u32,
}
struct PipelineState<T> {
@ -1544,6 +1605,10 @@ pub(crate) mod shader_resources {
#[cfg(debug_assertions)]
pub(super) fn build_shader_blob(entry: ShaderModule, target: ShaderTarget) -> Result<ID3DBlob> {
unsafe {
use windows::Win32::Graphics::{
Direct3D::ID3DInclude, Hlsl::D3D_COMPILE_STANDARD_FILE_INCLUDE,
};
let shader_name = if matches!(entry, ShaderModule::EmojiRasterization) {
"color_text_raster.hlsl"
} else {
@ -1572,10 +1637,15 @@ pub(crate) mod shader_resources {
let entry_point = PCSTR::from_raw(entry.as_ptr());
let target_cstr = PCSTR::from_raw(target.as_ptr());
// really dirty trick because winapi bindings are unhappy otherwise
let include_handler = &std::mem::transmute::<usize, ID3DInclude>(
D3D_COMPILE_STANDARD_FILE_INCLUDE as usize,
);
let ret = D3DCompileFromFile(
&HSTRING::from(shader_path.to_str().unwrap()),
None,
None,
include_handler,
entry_point,
target_cstr,
D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION,

View file

@ -1,7 +1,6 @@
use std::{
cell::RefCell,
ffi::OsStr,
mem::ManuallyDrop,
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
@ -18,10 +17,7 @@ use windows::{
UI::ViewManagement::UISettings,
Win32::{
Foundation::*,
Graphics::{
Gdi::*,
Imaging::{CLSID_WICImagingFactory, IWICImagingFactory},
},
Graphics::Gdi::*,
Security::Credentials::*,
System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*, Threading::*},
UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*},
@ -41,7 +37,6 @@ pub(crate) struct WindowsPlatform {
foreground_executor: ForegroundExecutor,
text_system: Arc<DirectWriteTextSystem>,
windows_version: WindowsVersion,
bitmap_factory: ManuallyDrop<IWICImagingFactory>,
drop_target_helper: IDropTargetHelper,
validation_number: usize,
main_thread_id_win32: u32,
@ -101,12 +96,8 @@ impl WindowsPlatform {
let foreground_executor = ForegroundExecutor::new(dispatcher);
let directx_devices = DirectXDevices::new(disable_direct_composition)
.context("Unable to init directx devices.")?;
let bitmap_factory = ManuallyDrop::new(unsafe {
CoCreateInstance(&CLSID_WICImagingFactory, None, CLSCTX_INPROC_SERVER)
.context("Error creating bitmap factory.")?
});
let text_system = Arc::new(
DirectWriteTextSystem::new(&directx_devices, &bitmap_factory)
DirectWriteTextSystem::new(&directx_devices)
.context("Error creating DirectWriteTextSystem")?,
);
let drop_target_helper: IDropTargetHelper = unsafe {
@ -128,7 +119,6 @@ impl WindowsPlatform {
text_system,
disable_direct_composition,
windows_version,
bitmap_factory,
drop_target_helper,
validation_number,
main_thread_id_win32,
@ -716,7 +706,6 @@ impl Platform for WindowsPlatform {
impl Drop for WindowsPlatform {
fn drop(&mut self) {
unsafe {
ManuallyDrop::drop(&mut self.bitmap_factory);
OleUninitialize();
}
}

View file

@ -1,6 +1,10 @@
#include "alpha_correction.hlsl"
cbuffer GlobalParams: register(b0) {
float4 gamma_ratios;
float2 global_viewport_size;
uint2 _pad;
float grayscale_enhanced_contrast;
uint _pad;
};
Texture2D<float4> t_sprite: register(t0);
@ -1098,7 +1102,8 @@ MonochromeSpriteVertexOutput monochrome_sprite_vertex(uint vertex_id: SV_VertexI
float4 monochrome_sprite_fragment(MonochromeSpriteFragmentInput input): SV_Target {
float sample = t_sprite.Sample(s_sprite, input.tile_position).r;
return float4(input.color.rgb, input.color.a * sample);
float alpha_corrected = apply_contrast_and_gamma_correction(sample, input.color.rgb, grayscale_enhanced_contrast, gamma_ratios);
return float4(input.color.rgb, input.color.a * alpha_corrected);
}
/*