Skip to content

Skia shader as a background fill variant#1743

Open
LynithDev wants to merge 5 commits intomarc2332:mainfrom
LynithDev:feat/shader-fill
Open

Skia shader as a background fill variant#1743
LynithDev wants to merge 5 commits intomarc2332:mainfrom
LynithDev:feat/shader-fill

Conversation

@LynithDev
Copy link
Copy Markdown

Adds support to use a Skia shader as a background for anything that derives StyleExt.

Also adds a UniformsBuilder with (what I think is) every type of Uniform SKSL accepts.

Example usage:

const SHADER: &str = r#"
uniform float3 iResolution;
uniform float iTime;

half4 main(float2 fragCoord) {
    float2 uv = fragCoord / iResolution.xy;

    half r = 0.5 + 0.5 * sin(iTime + uv.x * 3.14);
    half g = 0.5 + 0.5 * sin(iTime + uv.y * 3.14);
    half b = 0.5 + 0.5 * sin(iTime);

    return half4(r, g, b, 1.0);
}
"#;

fn app() -> impl IntoElement {
    let now = std::time::Instant::now();

    rect()
        .expanded()
        .background_shader(ShaderFill::new(
            SHADER,
            move |effect, bounds| {
                let mut builder = UniformsBuilder::default();
                builder.set(
                    "iResolution",
                    UniformValue::Float3(bounds.width(), bounds.height(), 0.),
                );

                builder.set("iTime", UniformValue::Float(now.elapsed().as_secs_f32()));

                let uniforms = builder.build(effect);

                effect.make_shader(skia_safe::Data::new_copy(&uniforms), &[], None)
            },
        ))
}

@LynithDev LynithDev requested a review from marc2332 as a code owner April 2, 2026 16:28
@marc2332 marc2332 moved this to Pending for Review in Freya Planning Apr 2, 2026
@marc2332 marc2332 added this to the 0.4.0 milestone Apr 2, 2026
@marc2332
Copy link
Copy Markdown
Owner

marc2332 commented Apr 2, 2026

You can format the code using just f

Comment thread crates/freya-core/src/style/shader.rs Outdated
if let Some(effect) = &self.effect
&& let Some(other_effect) = &other.effect
{
std::ptr::eq(effect.inner(), other_effect.inner())
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comparison won't really work if someone decides to compile the same shader source twice.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 2, 2026

Codecov Report

❌ Patch coverage is 0% with 66 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.19%. Comparing base (081f371) to head (56c8282).
⚠️ Report is 44 commits behind head on main.

Files with missing lines Patch % Lines
crates/freya-core/src/style/shader.rs 0.00% 56 Missing ⚠️
crates/freya-core/src/elements/extensions.rs 0.00% 4 Missing ⚠️
crates/freya-core/src/style/fill.rs 0.00% 4 Missing ⚠️
...tes/freya-devtools-app/src/components/attribute.rs 0.00% 1 Missing ⚠️
crates/freya-devtools/src/node_info.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1743      +/-   ##
==========================================
+ Coverage   60.65%   61.19%   +0.53%     
==========================================
  Files         305      311       +6     
  Lines       38577    39508     +931     
==========================================
+ Hits        23400    24177     +777     
- Misses      15177    15331     +154     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread crates/freya-core/src/style/shader.rs Outdated
Comment on lines +111 to +239
#[derive(Debug, Default, Clone, PartialEq)]
pub struct UniformsBuilder {
uniforms: std::collections::HashMap<String, UniformValue>,
}

#[derive(Debug, Clone, PartialEq)]
pub enum UniformValue {
// Scalar floats
Float(f32), // float
Float2(f32, f32), // float2
Float3(f32, f32, f32), // float3
Float4(f32, f32, f32, f32), // float4

// Scalar integers
Int(i32), // int
Int2(i32, i32), // int2
Int3(i32, i32, i32), // int3
Int4(i32, i32, i32, i32), // int4

// Boolean types
Bool(bool), // bool
Bool2(bool, bool), // bool2
Bool3(bool, bool, bool), // bool3
Bool4(bool, bool, bool, bool), // bool4

// Matrices
Mat2([f32; 4]), // float2x2
Mat3([f32; 9]), // float3x3
Mat4([f32; 16]), // float4x4

// Arrays
FloatArray(Vec<f32>),
IntArray(Vec<i32>),
BoolArray(Vec<bool>),
}

impl UniformsBuilder {
#[inline]
pub fn new() -> Self {
Self::default()
}

/// Set a uniform value.
pub fn set(&mut self, name: impl AsRef<str>, value: UniformValue) {
self.uniforms.insert(name.as_ref().to_string(), value);
}

pub fn build(self, shader: &RuntimeEffect) -> Vec<u8> {
let mut values = Vec::new();

for uniform in shader.uniforms() {
let value = self.uniforms.get(uniform.name()).unwrap_or_else(|| {
panic!(
"Uniform '{}' not found for shader. Available uniforms: {:?}",
uniform.name(),
self.uniforms.keys().collect::<Vec<_>>()
)
});

Self::push_uniform(value, &mut values);
}

values
}

#[rustfmt::skip]
fn push_uniform(value: &UniformValue, values: &mut Vec<u8>) {
macro_rules! push_f32 {
($x:expr) => { values.extend($x.to_le_bytes()) };
}
macro_rules! push_i32 {
($x:expr) => { values.extend($x.to_le_bytes()) };
}
macro_rules! push_bool32 {
($x:expr) => { values.extend(($x as u32).to_le_bytes()) };
}

match value {
// --- Scalars & Vectors ---
UniformValue::Float(f) => push_f32!(*f),
UniformValue::Float2(x,y) => { push_f32!(*x); push_f32!(*y); },
UniformValue::Float3(x,y,z) => { push_f32!(*x); push_f32!(*y); push_f32!(*z); },
UniformValue::Float4(x,y,z,w) => { push_f32!(*x); push_f32!(*y); push_f32!(*z); push_f32!(*w); },

UniformValue::Int(i) => push_i32!(*i),
UniformValue::Int2(x,y) => { push_i32!(*x); push_i32!(*y); },
UniformValue::Int3(x,y,z) => { push_i32!(*x); push_i32!(*y); push_i32!(*z); },
UniformValue::Int4(x,y,z,w) => { push_i32!(*x); push_i32!(*y); push_i32!(*z); push_i32!(*w); },

UniformValue::Bool(b) => push_bool32!(*b),
UniformValue::Bool2(x,y) => { push_bool32!(*x); push_bool32!(*y); },
UniformValue::Bool3(x,y,z) => { push_bool32!(*x); push_bool32!(*y); push_bool32!(*z); },
UniformValue::Bool4(x,y,z,w) => { push_bool32!(*x); push_bool32!(*y); push_bool32!(*z); push_bool32!(*w); },

// Matrices (column-major order)
UniformValue::Mat2(m) => {
push_f32!(m[0]); push_f32!(m[2]); push_f32!(m[1]); push_f32!(m[3]);
},
UniformValue::Mat3(m) => {
push_f32!(m[0]); push_f32!(m[3]); push_f32!(m[6]);
push_f32!(m[1]); push_f32!(m[4]); push_f32!(m[7]);
push_f32!(m[2]); push_f32!(m[5]); push_f32!(m[8]);
}
UniformValue::Mat4(m) => {
push_f32!(m[0]); push_f32!(m[4]); push_f32!(m[8]); push_f32!(m[12]);
push_f32!(m[1]); push_f32!(m[5]); push_f32!(m[9]); push_f32!(m[13]);
push_f32!(m[2]); push_f32!(m[6]); push_f32!(m[10]); push_f32!(m[14]);
push_f32!(m[3]); push_f32!(m[7]); push_f32!(m[11]); push_f32!(m[15]);
}

// --- Arrays (recursive) ---
UniformValue::FloatArray(items) => {
for f in items {
Self::push_uniform(&UniformValue::Float(*f), values);
}
}
UniformValue::IntArray(items) => {
for i in items {
Self::push_uniform(&UniformValue::Int(*i), values);
}
}
UniformValue::BoolArray(items) => {
for b in items {
Self::push_uniform(&UniformValue::Bool(*b), values);
}
}
}
}
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this, after seeing it now I dont see its value

Copy link
Copy Markdown
Author

@LynithDev LynithDev Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alternative to the example above would look a bit like this:

effect.make_shader(
    skia_safe::Data::new_copy(
        &[bounds.width().to_le_bytes(), bounds.height().to_le_bytes(), 0.0f32.to_le_bytes(), now.elapsed().as_secs_f32().to_le_bytes()].concat(),
    ),
    &[],
    None,
)

Yes the values must be in that specific order as well, as thats the order the uniforms in the shader were defined with.

I could make an extension function to RuntimeEffect instead which tries to do this using generics if possible

Copy link
Copy Markdown
Owner

@marc2332 marc2332 Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alternative to the example above would look a bit like this:

exactly, its very simple stuff

chore(core): Remove UniformsBuilder
Comment thread crates/freya-core/src/style/shader.rs Outdated

#[derive(Clone)]
pub struct ShaderFill {
effect: Option<RuntimeEffect>,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
effect: Option<RuntimeEffect>,

Comment on lines +25 to +26
unsafe impl Send for ShaderFill {}
unsafe impl Sync for ShaderFill {}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
unsafe impl Send for ShaderFill {}
unsafe impl Sync for ShaderFill {}

Comment thread crates/freya-core/src/style/shader.rs Outdated
where
S: ShaderProvider + Send + Sync + 'static,
{
let effect = RuntimeEffect::make_for_shader(sksl, None)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this instanciation part of ShaderFill ? Just let the user of this trait it himself, no?

Suggested change
let effect = RuntimeEffect::make_for_shader(sksl, None)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RuntimeEffect::make_for_shader can be very heavy and calling it every repaint is a bad idea.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair, by you currently create it on every component render, you should allow ShaderFill to take an opt-in RuntimeEffect then too, so that the user can cache it or something?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first part is true and yes I don't know how to properly store cache it without the user's input.

I guess it could.

Copy link
Copy Markdown
Owner

@marc2332 marc2332 Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could try allowing to pass Rc<RuntimeEffect>, its how I kinda do it in shader_editor.rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Pending for Review

Development

Successfully merging this pull request may close these issues.

2 participants