Skip to content

Add alpha support to Framebuffer#1

Draft
benjhar wants to merge 17 commits into
mainfrom
feature/composable_framebuffers
Draft

Add alpha support to Framebuffer#1
benjhar wants to merge 17 commits into
mainfrom
feature/composable_framebuffers

Conversation

@benjhar

@benjhar benjhar commented May 8, 2026

Copy link
Copy Markdown
Contributor

This PR adds support for alpha channels to our Framebuffer. It does so by making Framebuffer generic over C: Color, and adding an implementation of Color for RGBA.

There are some elements of this PR that warrant further discussion:

  • Implementing Color for f32 or u8, and/or other scalar types would enable use of grayscale Framebuffers. These could be useful for keeping track of alpha or brightness separately, or for use as a "mask".
  • The implementation, as it is, would allow us to vectorise most operations but would make it difficult to vectorise Framebuffer<RGBA>::alpha_blend and any operations that might seek to target only one channel, although we don't implement any such operations currently.
  • Color defines a blend function, which takes a rhs_percent parameter. The use of this parameter (or rather, how to use it properly) for blending RGBA values is not clear - RGBA values can be blended entirely using their alpha values. Do we just ignore those and have blend work consistently for both RGB and RGBA?

CC: @benhansen-io @tiesselune

@benhansen-io benhansen-io left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This will be nice to have. Still some high level questions but looking good overall.

Comment thread src/color.rs Outdated
Comment thread src/color.rs Outdated
Comment thread src/color.rs Outdated
Comment thread src/color.rs Outdated
Comment thread src/framebuffer.rs Outdated
Comment thread src/framebuffer.rs Outdated
Comment thread src/framebuffer.rs Outdated
Comment thread src/lights_setter.rs Outdated
@benjhar benjhar requested a review from benhansen-io May 11, 2026 11:57
@benjhar

benjhar commented May 11, 2026

Copy link
Copy Markdown
Contributor Author

Updated the PR comment with another question that came up.

@benhansen-io

Copy link
Copy Markdown
Member

Implementing Color for f32 or u8, and/or other scalar types would enable use of grayscale Framebuffers. These could be useful for keeping track of alpha or brightness separately, or for use as a "mask"

This is nice we can do it but I don't have any use case right now and can't think of any coming up so I would lean toward holding off for now. If we do end up wanting it, I think I would like to wrap a u8 in a Grayscale struct and impl Color on that instead of on u8 directly since it won't be common.

The implementation, as it is, would allow us to vectorise most operations but would make it difficult to vectorise Framebuffer::alpha_blend and any operations that might seek to target only one channel, although we don't implement any such operations currently.

Thats fine for now

Color defines a blend function, which takes a rhs_percent parameter. The use of this parameter (or rather, how to use it properly) for blending RGBA values is not clear - RGBA values can be blended entirely using their alpha values. Do we just ignore those and have blend work consistently for both RGB and RGBA?

Good point. I agree its confusing so I would prefer to not have it. How about removing it from Color and only implementing a free function blend(a: RGB, b: RGB) then we don't have to answer the ambiguous question for RGBA. That or maybe we rename it average and we take the component average of all components including alpha but I don't know when that would be useful. I am inclined to say lets not implement it for RGBA.

@benhansen-io

Copy link
Copy Markdown
Member

Does dim_to also feel weird for RGBA. I feel like my natural instinct would be that it would adjust the alpha channel but thats also weird.

@benjhar

benjhar commented May 11, 2026

Copy link
Copy Markdown
Contributor Author

I think I'd prefer renaming blend to average, over removing it as a method. At the moment it is just a component average, and so is dim_to with OFF. I think this is fine if we document it - for me the problem was that it is unclear, and for proper alpha blending we can have a separate alpha_blend(a: RGBA, b: RGBA) function.
I think it probably depends on which we think will be more common, RGB component-average, or RGBA alpha blending.

Comment thread src/lights_plane.rs Outdated
Comment thread src/lights_setter.rs Outdated
Comment thread src/lights_plane.rs Outdated
1. - ((relative_dist - FALL_OFF_DISTANCE) / (1.0 - FALL_OFF_DISTANCE))
};
let this_color = color::dim_to(c, percent_colored);
let this_color = c.dim_to(percent_colored);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So in my mind ideally we should be able to have a Framebuffer and write multiple draw calls to it and the falloff should use alpha and blend with the existing values. Otherwise it feels weird that we accept a writeable Framebuffer instead of just returning one.

Oh and this dim_to definitely feels like it should be alpha based for an RGBA buffer.

I don't have graphics API experience. I spent a bit of time asking Claude trying to figure out what is common here. Claude seems to indicate that they have configurable blend states that you can set globally or per pipeline depending on the API. Thats probably overkill for us, but we could pass in an optional blend fn pointer at some point so the user gets full control of how the existing pixel and drawed pictures get blended.

I really want to move these drawing functions to be a builder pattern and have fall_off be an option on the builder but I think that should be a separate PR and doesn't have to be you implementing it. So maybe we get these draw_* functions compatible with the current situation and then come back and improve them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So in my mind ideally we should be able to have a Framebuffer and write multiple draw calls to it and the falloff
should use alpha and blend with the existing values.

Oh and this dim_to definitely feels like it should be alpha based for an RGBA buffer.

Yeah this does sound much nicer than what we have at the moment. I'll work on making it so, hopefully without breaking anything. Agreed about dim_to.

I don't have graphics API experience.

I'm in the same boat. I agree that the approach Claude mentioned seems like a bit much. The optional function pointer would work but I also sort of don't like it. I can't think of a better option though.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Adding @tiesselune to this review since he has game dev experience in case he has any thoughts

@benjhar benjhar May 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The optional function pointer would work but I also sort of don't like it

I've come around on this - I think this should be fine, and it means we don't need two separate functions for scalar and "vector" blends and dims. Although, I don't think it needs to be optional - we can just pass it always and users can do |_| 0.5 if they want to just to scalar weighting

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So in my mind ideally we should be able to have a Framebuffer and write multiple draw calls to it and the falloff should use alpha and blend with the existing values. Otherwise it feels weird that we accept a writeable Framebuffer instead of just returning one.

@benhansen-io In your mind is it worth breaking these lights_plane functions for RGB to be able to implement this? I'm really struggling for a neat way to do this generically. The best way I can think of, so far, is to return to_rgb_u8 to Color and also add an alpha(&self) -> Option<u8> to Color. I can't think of a way that doesn't require adding extra functionality to Color.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've come around on this - I think this should be fine, and it means we don't need two separate functions for scalar and "vector" blends and dims. Although, I don't think it needs to be optional - we can just pass it always and users can do |_| 0.5 if they want to just to scalar weighting

What does |_| 0.5 represent. I thought the blending function would take two colors but I haven't throught through it all.

In your mind is it worth breaking these lights_plane functions for RGB to be able to implement this? I'm really struggling for a neat way to do this generically. The best way I can think of, so far, is to return to_rgb_u8 to Color and also add an alpha(&self) -> Option to Color. I can't think of a way that doesn't require adding extra functionality to Color.

The dim_to you have right now seems pretty close. The only thing wrong is it doesn't seem to blend with the current values in the framebuffer. For RGB it seems like instead of assuming we dim to black we should blend with the current color (which allows painting a black circle on a colored background for example) and for RGBA we should alpha blend the existing color and the new color (otherwise drawing red circle with falloff onto a green blackground would produce some black around the edges which doesn't make sense)

@benhansen-io benhansen-io requested a review from tiesselune May 11, 2026 15:12
@benhansen-io

Copy link
Copy Markdown
Member

Let me know when this is ready for another review.

@benjhar benjhar force-pushed the feature/composable_framebuffers branch from 5d4e04e to 43412b4 Compare May 14, 2026 14:08

@benhansen-io benhansen-io left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems like we are getting pretty close. This will be a nice change to have.

Marking request changes so its clear when you want me to look at it again.

Comment thread src/lights_plane.rs Outdated
1. - ((relative_dist - FALL_OFF_DISTANCE) / (1.0 - FALL_OFF_DISTANCE))
};
let this_color = color::dim_to(c, percent_colored);
let this_color = c.dim_to(percent_colored);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've come around on this - I think this should be fine, and it means we don't need two separate functions for scalar and "vector" blends and dims. Although, I don't think it needs to be optional - we can just pass it always and users can do |_| 0.5 if they want to just to scalar weighting

What does |_| 0.5 represent. I thought the blending function would take two colors but I haven't throught through it all.

In your mind is it worth breaking these lights_plane functions for RGB to be able to implement this? I'm really struggling for a neat way to do this generically. The best way I can think of, so far, is to return to_rgb_u8 to Color and also add an alpha(&self) -> Option to Color. I can't think of a way that doesn't require adding extra functionality to Color.

The dim_to you have right now seems pretty close. The only thing wrong is it doesn't seem to blend with the current values in the framebuffer. For RGB it seems like instead of assuming we dim to black we should blend with the current color (which allows painting a black circle on a colored background for example) and for RGBA we should alpha blend the existing color and the new color (otherwise drawing red circle with falloff onto a green blackground would produce some black around the edges which doesn't make sense)

Comment thread src/framebuffer.rs
pub struct Framebuffer {
#[derive(Clone, PartialEq)]
#[must_use]
pub struct Framebuffer<C: Color = RGB> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What do you think about a FramebufferRGBA type alias for Framebuffer::<RGBA>?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah that sounds useful

@benjhar

benjhar commented May 25, 2026

Copy link
Copy Markdown
Contributor Author

What does |_| 0.5 represent. I thought the blending function would take two colors but I haven't throught through it all.

|_| 0.5 would be the component average of both colours, with the function argument being the light index. I think taking both colours is better although it would make it more verbose.

For RGB it seems like instead of assuming we dim to black we should blend with the current color

I think this might be unintuitive, since dim_to only acts on self and just takes the percent as a parameter. This does sound useful though, what do you think about a separate function that does this more explicitly, like blend_with_current or light_blend. I think this would, internally, just be a call to weighted_average using the current lights as the rhs argument.

@benjhar benjhar force-pushed the feature/composable_framebuffers branch from 0bb96e1 to ea5ea15 Compare May 31, 2026 20:27
@benjhar

benjhar commented May 31, 2026

Copy link
Copy Markdown
Contributor Author

@benhansen-io I thought a bit more about this:

For RGB it seems like instead of assuming we dim to black we should blend with the current color

This would be fairly trivial if we used RGBA in MainFramebuffer instead of RGB. The memory overhead would be fairly small and we could use defaults for RGB flush. What do you think?

@benjhar benjhar requested a review from benhansen-io May 31, 2026 20:49

@benhansen-io benhansen-io left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am a little lost on this PR if its for feedback or for merging. When requesting a review its helpful if you tell me if its ready to merge or if you are looking for feedback and if your looking for feedback what items you want feedback on. If its ready to merge, please include all breaking changes and how we know they don't break activities and what tests were done to ensure that.

I like the direction this is going but I feel like this is too big with breaking changes to merge. I think we should try to merge the a minimal subset of this that makes no breaking changes and add in commits from there. Also sometimes I find it really helpful to make cleanup commits before making a change commit.

I also think handling RGB and RGBA in a generic context is going to be more confusing than its worth. I think Framebuffer being generic is fine for all of the set functions (its an assigmment). I think most of the current flush functions should only be implemented for Framebuffer.

Comment thread src/color.rs
}

#[allow(
clippy::cast_possible_truncation,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Going forward lets not put allow statements for pedantic clippy lints. They clutter up the code and are not run be default. For example there are more lines of code for this allow statement than the function definition and body.

Comment thread src/color.rs

/// Blend two colors together, according to their brightness or alpha.
#[must_use]
fn blend(self, rhs: Self) -> Self;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We renamed blend to weighted_average and then put a new definition of blend. Do you think this definition will work and will be better for all of our current usage sites or were you planning on renaming the current usage sites to weighted average?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is blending based on brightness a common operation? I don't have any intuition for it.When I asked Gemini how to mix two colors from 0 to 100% it only provided the weighted average option.

Comment thread src/color.rs
r: self.r,
g: self.g,
b: self.b,
a: mix_component(self.a, 0, percent),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This doesn't feel like a "dim". Its a dim if the background is black but who is to say that the background is black. Now that the flush for rgba is flushing with the current colors the background could be any color.

I am inclined to say we should define dim_to only in terms of RGB. In fact maybe we should only have this operation on RGB.

Comment thread src/color.rs

fn luminance(self) -> f32 {
brightness(self)
const MAX_MAGNITUDE_SQUARED: f32 = 255.0 * 255.0 * 3.0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Lets add tests for this and other functions that are easily testable in this PR.

Comment thread src/framebuffer.rs
/// Flush this buffer to lights, blending with the current state according to `blend_fn`.
/// `blend_fn` is passed the buffer's light color, the current light color, and the light index,
/// in that order.
pub fn flush_blend<W: Fn(RGB, RGB, usize) -> RGB>(&self, blend_fn: W) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we plan to use this function anywhere for RGB? It feel more like an RGBA function so I am inclined to leave it off until we need it.

Comment thread src/framebuffer.rs

/// Blends this framebuffer with the current [`MainFramebuffer`], and writes the result back to
/// this buffer.
pub fn blend_with_current(&mut self) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think we need this function unless you know of a use case. If we find we need it, I would rather have a function to blend two Framebuffers and let the users call get_currently_set

Comment thread src/framebuffer.rs
}

/// Flush the buffer's contents to lights, ignoring alpha level.
pub fn flush_no_alpha(&self) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we can remove this function and let them. Its pretty easy to call .to_rgb().flush()

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants