Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Format API / Transparency #241

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

jaysonmaw
Copy link

This is my answer to #17 and #98

WIP

I have only actually implemented the required changes for windows and mac os, as this was just an experiment to see if the idea I had would be doable. And those are the 2 platforms I have/use.
If you are on these platforms feel free to point your cargo.toml at the fork to try it out. (If you do want to try it out, keep in mind you have to disable the compatibility feature in your cargo.toml that is enabled by default)


High level overview of changes:

  • Added ability to tell backend that you want to use transparency/alpha channel
  • Created RGBX/RGBA structs that can be used on every platform without needing to think about how the platform stores the u32 as the conversion to each of the platforms happens automatically

Essentially the changes are as follows:

  • Added softbuffer::Surface::new_with_alpha() function that creates an alpha aware surface
  • Created RGBX and RGBA structs
    • Both structs have a Self::new() and Self::new_unchecked()
      • new() returns a result, with an error if any of the values passed in overflow a u8
      • new_unchecked()returns Self directly, silently ignoring any overflow, the same as if you had 300u32 as u8
      • The two functions take any primitive number type (u8,u16,u32,etc)
      • I took a quick look at the machine code, when passing in a u8 or u32 it appears to be just as performant as doing bit shifts yourself (Obviously new has some extra jmp for the overflow checks but new_unchecked is very clean). In the case that you pass u8's into new it has the same machine code as new_unchecked
      • The structs are #[Repr(C)] with 4 u8's in them. I then change the ordering of the r,g,b,x/a properties based on the platform, and then inside the bufferImpl I can transmute the [u32] to the appropriate type based on with/without alpha
  • Depreciated old api where you get access to a raw &mut [u32] buffer
    • Added feature compatibility to allow the old deref to still point to &mut [u32] buffer, enabled by default.
    • Without compatibility feature enabled, buffer now derefs to a &mut [RGBX] when called on a surface created by calling Surface::new() and &mut [RGBA] when called on a surface created by calling Surface::new_with_alpha()
    • Can call buffer.pixels_mut() function to access old style &mut [u32] even with compatibility feature disabled
  • Changed the backend impl for windows and macos
    • Added new_with_alpha method
    • Made required changes to tell backend that we are using alpha channel if called with new api, but still run the old way otherwise

Example:

Taking the winit.rs example and changing the following relevant lines shows how this works:

let window = winit_app::make_window(elwt, |w| w.with_transparent(true));

let context = softbuffer::Context::new(window.clone()).unwrap();
let surface = softbuffer::Surface::new_with_alpha(&context, window.clone()).unwrap();

(window, surface)
let mut buffer = surface.buffer_mut().unwrap();
for y in 0..height.get() {
    for x in 0..width.get() {
        let red = x % 255;
        let green = y % 255;
        let blue = (x * y) % 255;
        let alpha = x % 255;
        let index = y as usize * width.get() as usize + x as usize;
        // buffer[index] = blue | (green << 8) | (red << 16) | (alpha << 24); // <--- previous method
        buffer[index] = softbuffer::RGBA::new_unchecked(red, green, blue, alpha);

        //Using the RGBX type would not work, and would fail to compile as the rgb type is based on the surface constructor you called
        //buffer[index] = softbuffer::RGBX::new_unchecked(red, green, blue); // <-- Would fail to compile
    }
}

Final Thoughts:

This was just some testing I wanted to do for my own project, if this is not the direction this project wants to go that is absolutely fine, I just thought I would share my experiments on the subject. I only spent about a day on it so no hard feelings if this never gets used.

The rest of the backends I did not update to even compile with the changes I made, so as is even if you just want to use the old api, this will not work on this branch currently.

If you did go this direction, you would probably want some extra conversion functions on the new RGBX/A types to be able to set them directly with a u32. (At least I think? Maybe not?) Of course that has the same issue of before of having conversion/format issues, but you could definitely implement it in such a way that there is either no conversion and it is platform dependent, or there is auto conversions for platforms that don't match the norm. Or a mix of both.
You would also want to expose a buffer.pixels_rgb_mut() function just as a shim so that you can access the new methods while having the compatibility feature enabled, just to help the transition.

Side Note: Please excuse the horror I created in the backend_dispatch.rs file. That could probably be cleaned up to be a bit nicer, I just wanted to get it working though.

Thanks for coming to my Ted talk ❤️

… channel

Created RGBX/RGBA structs that can be used on every platform without needing to think about how the platform stores the u32 as the conversion to each of the platforms happens automatically
@notgull
Copy link
Member

notgull commented Sep 11, 2024

WIP

If this PR isn't ready to be merged yet please hit "convert to draft" to convert this to a draft PR.

@jaysonmaw jaysonmaw marked this pull request as draft September 11, 2024 01:15
@jaysonmaw
Copy link
Author

WIP

If this PR isn't ready to be merged yet please hit "convert to draft" to convert this to a draft PR.

Sorry about that. I don't use github much. It is a draft PR now. 👍

@ids1024
Copy link
Member

ids1024 commented Sep 11, 2024

If you did go this direction, you would probably want some extra conversion functions on the new RGBX/A types to be able to set them directly with a u32. (At least I think? Maybe not?) Of course that has the same issue of before of having conversion/format issues, but you could definitely implement it in such a way that there is either no conversion and it is platform dependent, or there is auto conversions for platforms that don't match the norm. Or a mix of both.

For using something like tiny_skia (https://docs.rs/tiny-skia/latest/tiny_skia/struct.PixmapMut.html), it's desirable to have a way to get a &[u8] of the image buffer (with the &[u32] interface, bytemuck at least can safely convert it). Though of course that's dependent on the underlying format used, and conversion may be needed in some cases.

I guess this API is generally not ideal for interoperability with other crates that do rendering (whether or not they use SIMD trickery), since they won't make use of the same types. It does seem like an interesting solution for some use cases though.

Copy link
Member

@madsmtm madsmtm left a comment

Choose a reason for hiding this comment

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

I don't have much opinion on the top-level API, though I will say:

  • I think anything we change should eventually be compatible with imgref
  • From a quick look this also looks overly complicated IMO, I especially dislike the duplicate_item, can't we use normal generics?

But the CoreGraphics impl looks fine, and I think that transparency is desired somehow.

src/backends/cg.rs Show resolved Hide resolved
@jaysonmaw
Copy link
Author

I guess this API is generally not ideal for interoperability with other crates that do rendering

Good feedback, I was only using the crate from the perspective of just wanting an easy/simple api for drawing some pixeles to the screen so this API was very much based around that. I'll take a look at the tiny_skia and imgref crates. I think I have some ideas that will work well and fit with the u32 conversion functions I mentioned. I think I can get something going that is no cost on the platforms that are argb.

  • From a quick look this also looks overly complicated IMO, I especially dislike the duplicate_item, can't we use normal generics?

Yeah I do agree with you there, I don't love the duplicate_item macro either. I originally had tried to write it using strictly generic's but in the end couldn't find a solution that didn't involve just throwing everything out. The problem is if its generic over A, then the A needs to actually be aware of the platform dependent changes needed to turn on alpha channels, and I couldn't figure out a way to get that to work, especially if different platforms required changes in different places. So instead I just created the two marker types of WithoutAlpha and WithAlpha that way each of the backends can easily implement for those 2 concrete types and change whatever they need to with their impl. The 'duplicate_item' just saves me from having to duplicate impl's where nothing actually changes, and if I change just those impl's to be generic it doesn't compile because it would require types further down the chain to also be generic which brings us back to problem 1.

In reality what was done here was all types were split into something like ______WithAlpha and ______WithoutAlpha, the current generic just hides that so we can treat the two types the same where alphaness does not matter.

Maybe I'm missing something there though, if anyone else can get it working with generics that would be obviously better. I'll also probably take another look, as I really just wanted to get something working with the API that I imagined, a lot of the code can be improved for sure from the concept but just wanted to see if there was any interest before I put more work into it.

Thanks! 👍

@jaysonmaw
Copy link
Author

Yeah I do agree with you there, I don't love the duplicate_item macro either. I originally had tried to write it using strictly generic's but in the end couldn't find a solution that didn't involve just throwing everything out. The problem is if its generic over A, then the A needs to actually be aware of the platform dependent changes needed to turn on alpha channels, and I couldn't figure out a way to get that to work, especially if different platforms required changes in different places. So instead I just created the two marker types of WithoutAlpha and WithAlpha that way each of the backends can easily implement for those 2 concrete types and change whatever they need to with their impl. The 'duplicate_item' just saves me from having to duplicate impl's where nothing actually changes, and if I change just those impl's to be generic it doesn't compile because it would require types further down the chain to also be generic which brings us back to problem 1.

In reality what was done here was all types were split into something like ______WithAlpha and ______WithoutAlpha, the current generic just hides that so we can treat the two types the same where alphaness does not matter.

Maybe I'm missing something there though, if anyone else can get it working with generics that would be obviously better. I'll also probably take another look, as I really just wanted to get something working with the API that I imagined, a lot of the code can be improved for sure from the concept but just wanted to see if there was any interest before I put more work into it.

Completely ignore me. I ended up staring at this a bit more and was able to completely remove the duplicate_item macro, in favor of using pure generics. Apparently I just needed to sleep on that. I'll keep looking at the other items we discussed.

moved all structs and windows/mac backends to new generics
added format conversion system, only 2 formats currently to test out.
exposed a few more pixels_ functions
added a winit_tiny_skia example script using new format
@jaysonmaw
Copy link
Author

jaysonmaw commented Sep 11, 2024

Alright, removed the ugly #[duplicate_item] macro, replaced with generics.

Started work on a format system, currently just implemented 2 formats, 1 of them specifically with tiny_skia in mind, so a u8 rgba format.

I added an example called winit_tiny_skia.rs if you want to try it out.

I also messed around in GodBolt to check/ensure that the conversion functions are essentially no ops if the format you ask for matches the base platform format. And the formats that do require conversion I leave up to the compiler to figure out the best way of doing that instead of trying to hand roll something and that appears to be working fine.

The format API looks something like this: (Copied from the winit_tiny_skia.rs example)

buffer.pixel_u8_slice_rgba(|u8_buffer_rgba: &mut [u8]| {
    let mut pixmap =
        PixmapMut::from_bytes(u8_buffer_rgba, width.get(), height.get())
            .unwrap();
    let mut paint = Paint::default();
    paint.set_color_rgba8(255, 0, 255, 0); 
    paint.anti_alias = true;

    let path = {
        let mut pb = PathBuilder::new();
        let RADIUS: f32 = (width.get().min(height.get()) / 2) as f32;
        let CENTER: f32 = (width.get().min(height.get()) / 2) as f32;
        pb.move_to(CENTER + RADIUS, CENTER);
        for i in 1..8 {
            let a = 2.6927937 * i as f32;
            pb.line_to(CENTER + RADIUS * a.cos(), CENTER + RADIUS * a.sin());
        }
        pb.finish().unwrap()
    };

    let mut stroke = Stroke::default();
    stroke.width = 24.0;
    stroke.line_cap = LineCap::Round;
    stroke.dash = StrokeDash::new(vec![20.0, 40.0], 0.0);

    pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
});

The format is wrapped in a closure incase it does need to do conversion, it has to convert back before actually presenting to the window so that it matches the platform's expected format, so this ensures that while also not doing any allocation, and in the case there is no conversion it's just free.

I did also briefly look at imgref, from what I can tell there shouldn't be any reason this wouldn't work with that, but I didn't get to test anything with that yet.

Here's an image made with tiny_skia, based somewhat off that example:
image
This shows off transparency working, both on the softbuffer side and tiny_skia, the background drawn with softbuffer and the star using tiny_skia with Blendmode::Clear to cut out a star.

All in all it seems to work pretty well, interested to hear any thoughts or questions.

Added some documentation with examples to better show how different parts work together
Added u32/8 conversions for formats and main RGBX/RGBA structs
Refactored RGBFormat impl's into macro as it was a bunch of repeated code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants