Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ blake3 = "1.8.2"
walkdir = "2.5.0"

[profile.dev.package]
jay-algorithms = { opt-level = 3 }
jay-algorithms = { opt-level = 3, codegen-units = 1, incremental = false }
smallvec = { opt-level = 3 }
egui = { opt-level = 3, debug = "line-tables-only" }
emath = { opt-level = 3, debug = "line-tables-only" }
Expand Down
1 change: 1 addition & 0 deletions algorithms/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
clippy::needless_lifetimes
)]

pub mod lut;
pub mod qoi;
pub mod rect;
pub mod tf;
Expand Down
63 changes: 63 additions & 0 deletions algorithms/src/lut.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
pub use generated::*;

#[rustfmt::skip]
mod generated;

#[inline(never)]
fn fill(res: &mut [f32], mut src_mul: f32, dst_mul: f32, tf: impl Fn(f32) -> f32 + Copy) {
src_mul /= (res.len() - 1) as f32;
#[expect(clippy::needless_range_loop)]
for i in 0..res.len() {
let v = i as f32 * src_mul;
let v = tf(v);
let v = v * dst_mul;
res[i] = v;
}
}
Comment thread
mahkoh marked this conversation as resolved.

#[inline(never)]
pub fn fill_lut_3d(m: [[f32; 4]; 4], size: usize) -> Vec<[u32; 4]> {
let mut out = vec![[0u32; 4]; size * size * size];
let mul = 1.0 / (size - 1) as f32;
let idx = 0;
#[rustfmt::skip]
Comment thread
mahkoh marked this conversation as resolved.
let v = [
m[0][3],
m[1][3],
m[2][3],
];
for b in 0..size {
let bf = b as f32 * mul;
let idx = idx * size + b;
let v = [
v[0] + m[0][2] * bf,
v[1] + m[1][2] * bf,
v[2] + m[2][2] * bf,
];
for g in 0..size {
let gf = g as f32 * mul;
let idx = idx * size + g;
let v = [
v[0] + m[0][1] * gf,
v[1] + m[1][1] * gf,
v[2] + m[2][1] * gf,
];
for r in 0..size {
let rf = r as f32 * mul;
let idx = idx * size + r;
let v = [
v[0] + m[0][0] * rf,
v[1] + m[1][0] * rf,
v[2] + m[2][0] * rf,
];
out[idx] = [
(v[0] as f64 * u32::MAX as f64) as u32,
(v[1] as f64 * u32::MAX as f64) as u32,
(v[2] as f64 * u32::MAX as f64) as u32,
0,
];
}
}
}
out
}
175 changes: 175 additions & 0 deletions algorithms/src/lut/generated.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
use crate::lut::fill;
use crate::tf::*;

#[inline(never)]
pub fn fill_eotf(res: &mut [f32], src_mul: f32, dst_mul: f32, i: AlgoEotf) {
match i {
AlgoEotf::Linear => fill(res, src_mul, dst_mul, eotfs::linear::<()>),
AlgoEotf::St2084Pq => fill(res, src_mul, dst_mul, eotfs::st2084_pq::<()>),
AlgoEotf::Gamma22 => fill(res, src_mul, dst_mul, eotfs::gamma22::<()>),
AlgoEotf::Gamma24 => fill(res, src_mul, dst_mul, eotfs::gamma24::<()>),
AlgoEotf::Gamma28 => fill(res, src_mul, dst_mul, eotfs::gamma28::<()>),
AlgoEotf::St240 => fill(res, src_mul, dst_mul, eotfs::st240::<()>),
AlgoEotf::Log100 => fill(res, src_mul, dst_mul, eotfs::log100::<()>),
AlgoEotf::Log316 => fill(res, src_mul, dst_mul, eotfs::log316::<()>),
AlgoEotf::St428 => fill(res, src_mul, dst_mul, eotfs::st428::<()>),
AlgoEotf::CompoundPower24 => fill(res, src_mul, dst_mul, eotfs::compound_power_2_4::<()>),
AlgoEotf::Bt1886(tp) => {
let tf = eotfs::bt1886::<()>(tp);
fill(res, src_mul, dst_mul, tf)
}
AlgoEotf::Pow(tp) => {
let tf = eotfs::pow::<()>(tp);
fill(res, src_mul, dst_mul, tf)
}
}
}

#[inline(never)]
pub fn fill_inv_eotf(res: &mut [f32], src_mul: f32, dst_mul: f32, i: AlgoEotf) {
match i {
AlgoEotf::Linear => fill(res, src_mul, dst_mul, inv_eotfs::linear::<()>),
AlgoEotf::St2084Pq => fill(res, src_mul, dst_mul, inv_eotfs::st2084_pq::<()>),
AlgoEotf::Gamma22 => fill(res, src_mul, dst_mul, inv_eotfs::gamma22::<()>),
AlgoEotf::Gamma24 => fill(res, src_mul, dst_mul, inv_eotfs::gamma24::<()>),
AlgoEotf::Gamma28 => fill(res, src_mul, dst_mul, inv_eotfs::gamma28::<()>),
AlgoEotf::St240 => fill(res, src_mul, dst_mul, inv_eotfs::st240::<()>),
AlgoEotf::Log100 => fill(res, src_mul, dst_mul, inv_eotfs::log100::<()>),
AlgoEotf::Log316 => fill(res, src_mul, dst_mul, inv_eotfs::log316::<()>),
AlgoEotf::St428 => fill(res, src_mul, dst_mul, inv_eotfs::st428::<()>),
AlgoEotf::CompoundPower24 => {
fill(res, src_mul, dst_mul, inv_eotfs::compound_power_2_4::<()>)
}
AlgoEotf::Bt1886(tp) => {
let tf = inv_eotfs::bt1886::<()>(tp);
fill(res, src_mul, dst_mul, tf)
}
AlgoEotf::Pow(tp) => {
let tf = inv_eotfs::pow::<()>(tp);
fill(res, src_mul, dst_mul, tf)
}
}
}

#[inline(never)]
pub fn fill_dual(res: &mut [f32], src_mul: f32, dst_mul: f32, i: AlgoEotf, o: AlgoEotf) {
#[inline(always)]
fn handle_dst(
res: &mut [f32],
src_mul: f32,
dst_mul: f32,
o: AlgoEotf,
sf: impl Fn(f32) -> f32 + Copy,
) {
match o {
AlgoEotf::Linear => {
fill(res, src_mul, dst_mul, |v| inv_eotfs::linear::<()>(sf(v)));
}
AlgoEotf::St2084Pq => {
fill(res, src_mul, dst_mul, |v| inv_eotfs::st2084_pq::<()>(sf(v)));
}
AlgoEotf::Gamma22 => {
fill(res, src_mul, dst_mul, |v| inv_eotfs::gamma22::<()>(sf(v)));
}
AlgoEotf::Gamma24 => {
fill(res, src_mul, dst_mul, |v| inv_eotfs::gamma24::<()>(sf(v)));
}
AlgoEotf::Gamma28 => {
fill(res, src_mul, dst_mul, |v| inv_eotfs::gamma28::<()>(sf(v)));
}
AlgoEotf::St240 => {
fill(res, src_mul, dst_mul, |v| inv_eotfs::st240::<()>(sf(v)));
}
AlgoEotf::Log100 => {
fill(res, src_mul, dst_mul, |v| inv_eotfs::log100::<()>(sf(v)));
}
AlgoEotf::Log316 => {
fill(res, src_mul, dst_mul, |v| inv_eotfs::log316::<()>(sf(v)));
}
AlgoEotf::St428 => {
fill(res, src_mul, dst_mul, |v| inv_eotfs::st428::<()>(sf(v)));
}
AlgoEotf::CompoundPower24 => {
fill(res, src_mul, dst_mul, |v| {
inv_eotfs::compound_power_2_4::<()>(sf(v))
});
}
AlgoEotf::Bt1886(tp) => {
#[inline(never)]
fn f(
res: &mut [f32],
src_mul: f32,
dst_mul: f32,
tp: f32,
sf: impl Fn(f32) -> f32 + Copy,
) {
let tf = inv_eotfs::bt1886::<()>(tp);
fill(res, src_mul, dst_mul, |v| tf(sf(v)))
}
f(res, src_mul, dst_mul, tp, sf);
}
AlgoEotf::Pow(tp) => {
#[inline(never)]
fn f(
res: &mut [f32],
src_mul: f32,
dst_mul: f32,
tp: f32,
sf: impl Fn(f32) -> f32 + Copy,
) {
let tf = inv_eotfs::pow::<()>(tp);
fill(res, src_mul, dst_mul, |v| tf(sf(v)))
}
f(res, src_mul, dst_mul, tp, sf);
}
}
}
match i {
AlgoEotf::Linear => {
handle_dst(res, src_mul, dst_mul, o, eotfs::linear::<()>);
}
AlgoEotf::St2084Pq => {
handle_dst(res, src_mul, dst_mul, o, eotfs::st2084_pq::<()>);
}
AlgoEotf::Gamma22 => {
handle_dst(res, src_mul, dst_mul, o, eotfs::gamma22::<()>);
}
AlgoEotf::Gamma24 => {
handle_dst(res, src_mul, dst_mul, o, eotfs::gamma24::<()>);
}
AlgoEotf::Gamma28 => {
handle_dst(res, src_mul, dst_mul, o, eotfs::gamma28::<()>);
}
AlgoEotf::St240 => {
handle_dst(res, src_mul, dst_mul, o, eotfs::st240::<()>);
}
AlgoEotf::Log100 => {
handle_dst(res, src_mul, dst_mul, o, eotfs::log100::<()>);
}
AlgoEotf::Log316 => {
handle_dst(res, src_mul, dst_mul, o, eotfs::log316::<()>);
}
AlgoEotf::St428 => {
handle_dst(res, src_mul, dst_mul, o, eotfs::st428::<()>);
}
AlgoEotf::CompoundPower24 => {
handle_dst(res, src_mul, dst_mul, o, eotfs::compound_power_2_4::<()>);
}
AlgoEotf::Bt1886(sp) => {
#[inline(never)]
fn f(res: &mut [f32], src_mul: f32, dst_mul: f32, o: AlgoEotf, sp: f32) {
let sf = eotfs::bt1886::<()>(sp);
handle_dst(res, src_mul, dst_mul, o, sf);
}
f(res, src_mul, dst_mul, o, sp);
}
AlgoEotf::Pow(sp) => {
#[inline(never)]
fn f(res: &mut [f32], src_mul: f32, dst_mul: f32, o: AlgoEotf, sp: f32) {
let sf = eotfs::pow::<()>(sp);
handle_dst(res, src_mul, dst_mul, o, sf);
}
f(res, src_mul, dst_mul, o, sp);
}
}
}
16 changes: 16 additions & 0 deletions algorithms/src/tf.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
#![expect(clippy::excessive_precision)]

#[derive(Copy, Clone)]
pub enum AlgoEotf {
Linear,
St2084Pq,
Bt1886(f32),
Gamma22,
Gamma24,
Gamma28,
St240,
Log100,
Log316,
St428,
Pow(f32),
CompoundPower24,
}

pub fn bt1886_eotf_args<T>(c: f32) -> [f32; 4] {
let gamma = 1.0 / 2.4;
let a1 = 1.0 / (1.0 - c);
Expand Down
8 changes: 8 additions & 0 deletions book/src/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,14 @@ Toggle direct scanout:
~$ jay randr card card0 direct-scanout disable
```

Toggle plane color pipelines (offloads color conversion to the display
hardware; only effective on supported devices):

```shell
~$ jay randr card card0 plane-color-pipelines enable
~$ jay randr card card0 plane-color-pipelines disable
```

Adjust the page-flip margin (in milliseconds, default 1.5):

```shell
Expand Down
23 changes: 23 additions & 0 deletions book/src/configuration/gpu.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,28 @@ direct-scanout = false
direct-scanout = false
```

## Plane color pipelines

Some display hardware can perform color-space and HDR conversions directly in
the scanout plane's color pipeline, instead of Jay performing them during
composition. Offloading this work to the hardware lets color-managed and
[HDR](../hdr.md) content reach the display through
[direct scanout](#direct-scanout), avoiding a composition pass and reducing
power usage.

This is disabled by default and only takes effect on hardware that exposes a
suitable color pipeline. `jay randr` reports whether a device supports it and
whether it is currently in use.

```toml
[[drm-devices]]
match = {
pci-vendor = 0x1002,
pci-model = 0x73ff,
}
plane-color-pipelines = true
```

## Flip margin

The flip margin is the time (in milliseconds) between the compositor
Expand Down Expand Up @@ -246,6 +268,7 @@ Use `jay randr` subcommands:
```shell
~$ jay randr card <card> api vulkan
~$ jay randr card <card> direct-scanout enable
~$ jay randr card <card> plane-color-pipelines enable
~$ jay randr card <card> primary
```

Expand Down
3 changes: 3 additions & 0 deletions book/src/control-center.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@ Primary Device
Direct Scanout
: Toggle direct scanout (bypasses composition for lower latency)

Color Pipelines
: Toggle offloading color conversion to the display hardware's color pipeline. Only shown for devices that support it.

Flip Margin
: Adjust the page-flip margin in milliseconds, with +/- buttons for 0.1 ms steps

Expand Down
15 changes: 15 additions & 0 deletions book/src/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,18 @@ on unusual hardware or for diagnosing problems.
`JAY_NO_DESCRIPTOR_HEAP`
: Set to `1` to disable the use of Vulkan descriptor heaps, even on devices
that support them.

### Hardware color management

These variables control Jay's use of the display hardware's color pipeline on
the DRM backend.

`JAY_MCM_AMD_ALLOW_CURSOR`
: Set to `1` to use the color pipeline on AMD while a hardware cursor is
present.

`JAY_MCM_AMD_USE_FIRST_LUT`
: Set to `1` to use the first 1D LUT in the color pipeline on AMD.

`JAY_MCM_NVIDIA_USE_LUTS`
: Set to `1` to use the 1D LUTs in the color pipeline on NVIDIA.
1 change: 1 addition & 0 deletions codegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ regex = { version = "1.12.4", default-features = false, features = ["std", "unic
linearize = { version = "0.1.7", default-features = false, features = ["derive"] }
kbvm = { version = "0.1.7", default-features = false }
tempfile = { version = "3.27.0", default-features = false }
with_builtin_macros = { version = "0.1.0", default-features = false }
Loading
Loading