-
Notifications
You must be signed in to change notification settings - Fork 24
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
Add API to access Bitmap
pixel and mask data
#59
base: main
Are you sure you want to change the base?
Conversation
# Breaking changes - Adds a lifetime to `BitmapData`, so it is no longer easy to store in persistent structs. `BitmapData` is not allowed to outlive the `Bitmap` that it came from. - Makes all `BitmapData` fields private so they cannot be changed by callers. This is a safety invariant for accessing the underlying pixel and mask buffers. # New features - `BitmapData::pixels()` and `BitmapData::mask()` provides access to the underlying pixel and mask data as byte slices. - `BitmapData` has getter methods for its fields. - Adds `Bitmap::get_data_mut()` to gain mutable access to the pixel and mask data through `BitmapDataMut`. - `BitmapData::to_view()` and `BitmapDataMut::to_view()` are available to create a view of the `BitmapData` or `BitmapDataMut` that is not tied to the lifetime of the `Bitmap` that owns the data. `BitmapDataView` relinquishes access to the pixel and mask data so that it can be easily persisted and outlive the `Bitmap`. # Oddities - `BitmapInner` has a number of public methods, but this type is never made accessible publically. These methods should either be made private or `pub(crate)`. Also, `BitmapInner` supports more methods than the public `Bitmap` type. - `Bitmap::get_data_mut()` technically does not need an exclusive reference to `Self`, since we can rely on `borrow_mut()` panicking at runtime when attempting to acquire two `BitmapDataMut`s from the same `Bitmap`. But it is a better user experience to enforce the invariant at compile time. - `BitmapDataMut` cannot exist more than once for the same `Bitmap`, as mentioned above. This would cause UB by allowing mutable aliasing by e.g. holding two references from `a.pixels_mut()` and `b.pixels_mut()`. These references will point to the same location in memory.
I think this implementation is correct. The somewhat limited testing I have done hasn't revealed any showstoppers. And I believe my reasoning for the lifetimes and shared/exclusive splits in the API are sound. As a side note, an alternative to making Happy to discuss if anything here needs further improvement. I'll save the visibility weirdness mentioned in the "oddities" section for another conversation. |
I'm going to revert this PR back to draft while I sort out potential unsoundness that I discovered. I'll have a full analysis later. |
Ok, it is guaranteed Undefined Behavior that I found. The issue is that the A trivial example: // Create a small bitmap
let gfx = Graphics::get();
let bitmap = gfx.new_bitmap(
ScreenSize::new(16, 1),
LCDColor::Solid(LCDSolidColor::kColorBlack),
)?;
let pixels = bitmap.get_data()?.pixels();
// We know that `pixels` is all black.
assert_eq!(pixels, &[0, 0, 0, 0]);
// It is unsound to mutate the memory backing the slice while we hold onto it:
bitmap.clear(LCDColor::Solid(LCDSolidColor::kColorWhite))?;
// This assert should panic!
assert_eq!(pixels, &[0xff, 0xff, 0xff, 0xff]);
log_to_console!("Whoops! UB: {pixels:#04x?}"); Running this code, we see that neither assertion panics, and the log prints:
We were able to mutate the contents behind a shared reference, violating the Shared XOR Mutable principle. There are broadly three solutions that I know of for this: First OptionFirst, we might consider just marking the Second OptionThe second is to just accept interior mutability as an unchangeable fact and make The optimization tradeoff and the ergonomics tradeoff are too much for the only benefit (existing APIs with interior mutability don't need to be changed) to hold up, IMHO. Third OptionThe last alternative is going back on the original interior mutability design and require the user-facing API to take exclusive references ( Assuming we use this new pub fn clear(&mut self, color: LCDColor) -> Result<(), Error> {
self.inner.borrow().clear(color)
} We can still trivially obverse UB by cloning the bitmap and clearing the clone: // It is unsound to mutate the memory backing the slice, but we can clear it to white through a cloned `Rc`:
let mut bitmap2 = bitmap.clone();
bitmap2.clear(LCDColor::Solid(LCDSolidColor::kColorWhite))?; To fix this, we need This does work, but there are a few minor ergonomics issues:
Overall, I think the tradeoff between slight ergonomics issues and having a fully sound interface for accessing bitmap pixel data with low overhead is a clear winner. ConclusionMy conclusion, and my personal preference, is that making this API available is a good thing, but it cannot be at the expense of soundness or safety. The existence of ref-counting adds an extra layer of concern that requires holding on to the One could argue that a fourth option is removing the reference counting. That is more work than I am willing to put in here, and it would make Disregarding those issues, I will move forward with Footnotes
|
A full analysis is provided in a PR comment. The TLDR is: - Methods with interior mutability have been changed to take exclusive borrows. - `BitmapData` and `BitmapDataMut` now keep the `RefCell` borrows alive so that mutating cloned bitmaps will panic at runtime if the borrow is still held.
That looks solid. You don’t have |
Ah, I was in the middle of this rabbit hole myself until I discovered your PR. Nice Work! Personally, I'd prefer Option 3, as it seems the most idiomatic. Clone should in my opinion be a full clone of the Bitmap anyways, which would then allocate a new pixel buffer, if that is possible using the playdate APIs. Otherwise, it isn't a Clone, but a (weird) reference! |
I'm agree and... Ha! Here is example! |
Another problem with my implementation that I mentioned above is that I don't allow clone for "don't free on drop" bitmaps, That problem is actual for subject implementation. |
Breaking changes
BitmapData
, so it is no longer easy to store in persistent structs.BitmapData
is not allowed to outlive theBitmap
that it came from.BitmapData
fields private so they cannot be changed by callers. This is a safety invariant for accessing the underlying pixel and mask buffers.Bitmap::clear()
andBitmap::load()
now borrowself
and theBitmapInner
exclusively because these methods mutate the underlying bitmap data.New features
BitmapData::pixels()
andBitmapData::mask()
provides access to the underlying pixel and mask data as byte slices.BitmapData
has getter methods for its fields.Bitmap::get_data_mut()
to gain mutable access to the pixel and mask data throughBitmapDataMut
.BitmapData::to_view()
andBitmapDataMut::to_view()
are available to create a view of theBitmapData
orBitmapDataMut
that is not tied to the lifetime of theBitmap
that owns the data.BitmapDataView
relinquishes access to the pixel and mask data so that it can be easily persisted and outlive theBitmap
.Oddities
BitmapInner
has a number of public methods, but this type is never made accessible publically. These methods should either be made private orpub(crate)
. Also,BitmapInner
supports more methods than the publicBitmap
type.Bitmap::get_data_mut()
technically does not need an exclusive reference toSelf
, since we can rely onborrow_mut()
panicking at runtime when attempting to acquire twoBitmapDataMut
s from the sameBitmap
. But it is a better user experience to enforce the invariant at compile time.BitmapDataMut
cannot exist more than once for the sameBitmap
, as mentioned above. This would cause UB by allowing mutable aliasing by e.g. holding two references froma.pixels_mut()
andb.pixels_mut()
. These references will point to the same location in memory.