-
Notifications
You must be signed in to change notification settings - Fork 69
android: Make the backend zero-copy #331
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
base: master
Are you sure you want to change the base?
Conversation
On macOS, the locking/unlocking operation that So I don't think there are other platforms where this is a problem, Android is a bit special in "managing" the buffer(s) for us like this. Maybe an option would be to store the locked buffer on the surface? Something like: struct AndroidImpl {
native_window: NativeWindow,
in_progress_buffer: Option<NativeWindowBufferLockGuard<'static>>, // + unsafe magic
}
fn buffer_mut() {
if let Some(native_window_buffer) = self.in_progress_buffer {
return BufferImpl { native_window_buffer };
}
let native_window_buffer = self.native_window.lock(None)?;
BufferImpl { native_window_buffer, surface_in_progress_buffer: &mut self.in_progress_buffer }
}
impl Drop for BufferImpl {
fn drop(&mut self) {
let buffer = self.native_window_buffer.take();
*self.surface_in_progress_buffer = Some(buffer);
}
}That would allow the following to work the same as on other platforms: let mut buffer = surface.buffer_mut();
buffer.pixels().fill(Pixel::rgb(0, 0, 0)); // Clear
drop(buffer);
let mut buffer = surface.buffer_mut();
draw(&mut buffer);
buffer.present(); |
| // TODO: Validate that Android actually initializes (possibly with garbage) the buffer, or | ||
| // let softbuffer initialize it, or return MaybeUninit<Pixel>? | ||
| // i.e. consider age(). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As long as android is using a swap chain of two or three buffers and we can get the buffer age, it probably makes the most sense for softbuffer to zero-initialize the buffers, if Android doesn't already do that.
It shouldn't really add any meaningful cost to zero each buffer once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can somehow infer the amount of buffers with ANativeWindow_getBuffersDataSpace?
But I'd also be fine with zero-initializing in buffer_mut for now, and coming back to this later, this PR will be an improvement anyhow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at https://developer.android.com/ndk/reference/group/a-native-window#anativewindow_lock, I guess it would return the whole bounds of the buffer in inOutDirtyBounds if a buffer is freshly allocated?
Though the API seems a bit backwards from how we have softbuffer work. We don't have a damage region to pass until we present.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though the API seems a bit backwards from how we have
softbufferwork. We don't have a damage region to pass until we present.
Yeah, that's discussed in a code comment too:
softbuffer/src/backends/android.rs
Lines 151 to 158 in afa87bb
| // TODO: Android requires the damage rect _at lock time_ | |
| // Since we're faking the backing buffer _anyway_, we could even fake the surface lock | |
| // and lock it here (if it doesn't influence timings). | |
| // | |
| // Android seems to do this because the region can be expanded by the | |
| // system, requesting the user to actually redraw a larger region. | |
| // It's unclear if/when this is used, or if corruption/artifacts occur | |
| // when the enlarged damage region is not re-rendered? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems the implementation of this is in Surface::lock defined in https://android.googlesource.com/platform/frameworks/native/+/main/libs/gui/Surface.cpp.
Which also has:
const bool canCopyBack = (frontBuffer != nullptr &&
backBuffer->width == frontBuffer->width &&
backBuffer->height == frontBuffer->height &&
backBuffer->format == frontBuffer->format);And then copies the front buffer to the back buffer if it can.
Or otherwise:
// if we can't copy-back anything, modify the user's dirty
// region to make sure they redraw the whole bufferMaybe a bit of an abuse of the API, but if we pass an empty inOutDirtyBounds region, maybe we could then see if we get an empty region in return. In which case we can treat it as age 1. Otherwise treat it as a new buffer with age 0, which we can also zero-initialize to if we think that's necessary.
Sounds like that would work? And should generally return an age of 1, keeping things very simple for any damage-tracked renderer using softbuffer, since Android is apparently copying the front buffer to the back buffer already.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also unclear what would happen on a resize (which also changes the format). If the "view" size changes but resize is not called, does it reuse the same buffers with implied upscaling?
(On wgpu/glutin I saw that this format change implicitly limited EGL to only return configs for that format, so it was definitely confusing)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case, isn't it expected or allowed that nothing of the buffer is presented, because nothing was communicated or expected to have changed?
I was thinking the input dirty bounds is only used to calculate the output dirty bounds, but looking again the dirty region is also passed to backBuffer->lockAsync. So I guess we do need to just pass NULL (equivalent to the whole region).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay so the only remaining thing here is to zero-initialize it (always) to get rid of the MaybeUninit "safely", and leave the age() at 0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay so the only remaining thing here is to zero-initialize it (always) to get rid of the
MaybeUninit"safely", and leave theage()at0.
Yes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, as long as it performs better than the current version of the backend, always zero-initing and returning age 0 is a good incremental improvement to the backend, even if it might be nice to find a better way to do this in the future.
|
@madsmtm something like that would work. I dropped the Unfortunately that requires various unwraps (like your suggested |
madsmtm
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately that requires various unwraps
I'm totally fine with a bit of unwrapping, especially after #313, where we don't need to make pixels_mut zero-cost.
should not be susceptible to
std::mem::forget()scenarios (which are safe) where unlock is not called andAndroidImpl::in_progess_bufferremainsNone?
I don't think my solution would've caused unsoundness either if you forget the buffer? It'd just cause a panic because you'd try to lock the buffer twice.
Anyhow, forgetting the buffer is definitely unsupported, as long as things are sound I don't care what the behavior of it is - I'll leave that up to whatever is easiest for you to maintain.
I mostly wanted to perhaps have a wrapper function around this accessor, to have one clear place to document that the And again, if it wasn't for
I didn't particularly say that your suggestion is unsound, I explicitly named them "scenarios (which are safe)" but they will always error in the way you described.
At least with my variant - where |
73cc135 to
acc2bcf
Compare
WIP but tested change that closes #318.
Still need to discuss how to handle
MaybeUninit, and the now side-effect presenting in-progress modifications topixels_mut()if the caller ended up dropping theirBufferhalf-way through, which is an unfortunate caveat of having a locked buffer for the surface that unlocks and presents on drop. The turning point is that it's not allowed tolock()a surface twice beforeunlock()(ing) and inherently presenting it.Are other platforms affected by such a lock-unlock kind of API? As hinted in #318
ASurfaceControl+ASurfaceTransaction+AHardwareBuffercompletely obviate this issue, but that has very high Android requirements (the API's are there for a while, but I seem to have been the first one ever using it on the rootSurface, the one you get fromNativeActivity, and it didn't work until a bug report and fix since Android 15 (API 35)).