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

Add screenshot api #7163

Merged
merged 24 commits into from Apr 19, 2023
Merged

Add screenshot api #7163

merged 24 commits into from Apr 19, 2023

Conversation

TheRawMeatball
Copy link
Member

@TheRawMeatball TheRawMeatball commented Jan 11, 2023

Fixes #1207

Objective

Right now, it's impossible to capture a screenshot of the entire window without forking bevy. This is because

  • The swapchain texture never has the COPY_SRC usage
  • It can't be accessed without taking ownership of it
  • Taking ownership of it breaks a lot of stuff

Solution

  • Introduce a dedicated api for taking a screenshot of a given bevy window, and guarantee this screenshot will always match up with what gets put on the screen.

Changelog

  • Added the ScreenshotManager resource with two functions, take_screenshot and save_screenshot_to_disk

@IceSentry IceSentry added A-Rendering Drawing game state to the screen C-Testing A change that impacts how we test Bevy or how users test their apps labels Jan 11, 2023
Copy link
Contributor

@StarArawn StarArawn left a comment

Choose a reason for hiding this comment

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

Looks pretty good over all. Can we add an example showcasing this? Right now it's a little hard to test and verify the work.

@@ -128,6 +138,11 @@ fn extract_windows(
for closed_window in closed.iter() {
extracted_windows.remove(&closed_window.id);
}
for (window, screenshot_func) in screenshot_manager.callbacks.lock().drain() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably should use try_lock here to avoid a deadlock if a screenshot is being taken at the same time? Although I guess the bevy scheduler prevents that from happening here..

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, this can't really happen because callbacks is pub(crate) and this is the singular callsite where it's called. Even if somehow a user ended up with two of these systems, one would simply take the lock and they'd run in series. Only one lock isn't enough to cause a deadlock.

Copy link
Member

Choose a reason for hiding this comment

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

Nile, your comment here would make a great comment for this line of code.

@@ -196,6 +197,18 @@ impl Image {
self.data,
)
.map(DynamicImage::ImageRgba8),
TextureFormat::Bgra8UnormSrgb => ImageBuffer::from_raw(
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we introduce the TextureFormat::Bgra8UnormSrgb here? Is there a reason bgra might be preferred over rgba?

Copy link
Member

Choose a reason for hiding this comment

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

Seconding the confusion about why this is part of this PR. I don't mind it, just a bit confused.

Copy link
Member Author

Choose a reason for hiding this comment

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

wgpu apparently quite likes this format for swapchain textures - this was necessary to get the save to disk functionality working on my machine.

Copy link
Member

Choose a reason for hiding this comment

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

Interesting. Can you please leave a comment here to that effect then?

Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

Useful functionality, generally looks solid. I'd like to see a top-level example before this gets merged.

Code quality and docs otherwise LGTM. I'll defer to rendering experts on the implementation strategy.

@mockersf
Copy link
Member

crashes for me on macOS:

thread 'main' panicked at 'Error in Surface::configure: requested usage is not supported', wgpu-0.14.2/src/backend/direct.rs:274:9

@TheRawMeatball
Copy link
Member Author

Changed the code to ensure a panic like the one @mockersf encountered is no longer possible, but it's a lot more code now.

@alice-i-cecile alice-i-cecile added this to the 0.10 milestone Jan 12, 2023
@@ -33,6 +33,9 @@ pub fn render_system(world: &mut World) {
render_device.clone(), // TODO: is this clone really necessary?
&render_queue.0,
world,
|encoder| {
crate::view::screenshot::submit_screenshot_commands(world, encoder);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason why you didn't just import the screenshot module?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not really, other than not wanting to pollute the namespace i guess.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fair, I don't mind either way. I'd prefer having the screenshot module in scope just to not have crate::view, but really it's up to you, really not a big deal.

@mockersf
Copy link
Member

Changed the code to ensure a panic like the one @mockersf encountered is no longer possible, but it's a lot more code now.

Works for me now! Thanks

@mockersf
Copy link
Member

mockersf commented Jan 12, 2023

There is an issue around screenshooting something with HDR & bloom enabled. I enabled them in example animated_fox

OS Screenshot

Screenshot 2023-01-12 at 22 58 03

This PR Screenshot

test

@mockersf
Copy link
Member

Crashes in WASM too:

Caused by:
    In a RenderPass
      note: encoder = `<CommandBuffer-(0, 726, Gl)>`
    In a set_pipeline command
      note: render pipeline = `<RenderPipeline-(0, 1, Gl)>`
    Render pipeline targets are incompatible with render pass
    Incompatible color attachment: the renderpass expected [Some(Rgba8UnormSrgb)] but was given [Some(Bgra8UnormSrgb)]

@alice-i-cecile alice-i-cecile added the D-Complex Quite challenging from either a design or technical perspective. Ask for help! label Jan 13, 2023
@TheRawMeatball
Copy link
Member Author

There is an issue around screenshooting something with HDR & bloom enabled. I enabled them in example animated_fox.

This seems to be a result of the brightness values being stored in the alpha channel - the two images are once again identical when alpha is disabled.

@alice-i-cecile
Copy link
Member

Thanks for putting this together, @TheRawMeatball. What are the blockers for merging this?

This needs review mostly, especially from folks with rendering expertise. This ended up trickier than I would have expected.

Community review counts, so please feel free to go over this carefully and approve it if you think it's ready :)

@mockersf
Copy link
Member

the PR also needs to be updated for #7681 I think

@hymm
Copy link
Contributor

hymm commented Apr 16, 2023

What's the current status of this PR?

@WannaBeKratos
Copy link

Does this snippet work without the WinitPlugin ?
// camera commands.spawn(Camera3dBundle { camera_3d: Camera3d { clear_color: ClearColorConfig::Custom(Color::WHITE), ..default() }, camera: Camera { // render before the "main pass" camera order: -1, target: RenderTarget::Image(image_handle), ..default() }, transform: Transform::from_xyz(0.0, 50.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y), ..default() }); }

@mockersf
Copy link
Member

@TheRawMeatball I think there has been an issue with your merge

@DGriffin91
Copy link
Contributor

What's the purpose of the render pass? Is it to avoid putting TextureUsages::COPY_SRC on the render target?

@IceSentry IceSentry self-requested a review April 18, 2023 20:12
Copy link
Contributor

@IceSentry IceSentry left a comment

Choose a reason for hiding this comment

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

I feel like there should probably be a bit more docs in screenshot.rs, but in general the code works and I haven't seen anything obviously wrong.

I don't fully like the idea of passing a finalizer callback to the graph runner, but I'm not sure how to avoid that.

Copy link
Member

@cart cart left a comment

Choose a reason for hiding this comment

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

I think this is a reasonable solution to the specific problem of taking screenshots of a window. But I don't love how "hacked in" it is. I think this impl points to a fundamental "render output feedback" problem that should (ultimately) be solved generically. The screenshot api is pretty opinionated and isn't a full solve to the problem. People should be able to build similar abstractions on their own.

I see a number of paths forward:

  • Continue down the finalizer path, but just modularize it (ex let people add arbitrary functions to the process via some new abstraction)
  • Build this into our graph abstraction ... this would mean screenshotting would not be a global feature, but rather one that exists in specific graphs ... this seems ok to me. We could build common "feedback" nodes (including a screenshot node). It seems to me like this PR could have already been expressed that way, rather than adding the finalizer. Functionally submit_screenshot_commands is really equivalent to a "render graph node that exists in every graph and always runs last". This doesn't solve the "window / swap chain / image management stuff" though, which is definitely the bigger challenge. It feels like a chunk of this could/should be expressed as some generic graph node that says "at this point in the graph, write this COPY_SRC texture view to a buffer of appropriate size and (maybe optionally) create an image from it". That leaves the problem of "writing the swap chain to a COPY_SRC texture" to be solved on its own in whatever way we deem "best" / "most modular"/ whatever we choose to optimize for.
  • Give up on reading swap chain outputs and instead read ViewTarget::maint_target_textures outputs in render graph nodes. Seems like we could make those COPY_SRC (maybe in an opt-in fashion when screenshots are enabled). This does ignore the upscaling step though (which does seem like a problem).

That being said, creating a generic abstraction is not a blocker (and I understand there is a fair amount of implementation nuance here when it comes to swapchain access). This is totally fine for now.

@cart cart added this pull request to the merge queue Apr 19, 2023
Merged via the queue into bevyengine:main with commit 9db70da Apr 19, 2023
23 of 24 checks passed
@mockersf mockersf mentioned this pull request Apr 22, 2023
cart pushed a commit that referenced this pull request Apr 28, 2023
# Objective

- Enable taking a screenshot in wasm
- Followup on #7163 

## Solution

- Create a blob from the image data, generate a url to that blob, add an
`a` element to the document linking to that url, click on that element,
then revoke the url
- This will automatically trigger a download of the screenshot file in
the browser
@zimond
Copy link

zimond commented Jun 4, 2023

Still could this work without a window? It seems some questions in the comments are unanswered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Rendering Drawing game state to the screen C-Testing A change that impacts how we test Bevy or how users test their apps D-Complex Quite challenging from either a design or technical perspective. Ask for help!
Projects
Status: Responded
Development

Successfully merging this pull request may close these issues.

Output screen texture to png