use crate::common::{WebviewIoSurface, WebviewSize, WebviewSource, WebviewTextureTarget};
use crate::prelude::{WebviewExtendStandardMaterial, WebviewSurface};
use crate::webview::texture_target::{WebviewGpuImageInjectSet, WebviewTextureSlot};
use crate::webview::ui::WebviewUiMaterial;
use bevy::asset::{AssetId, RenderAssetUsages};
use bevy::platform::collections::{HashMap, HashSet};
use bevy::prelude::*;
use bevy::render::{
Extract, Render, RenderApp, RenderSystems,
erased_render_asset::prepare_erased_assets,
render_asset::{RenderAssets, prepare_assets},
render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext, RenderLabel},
render_resource::{Extent3d, TextureFormat},
renderer::{RenderContext, RenderDevice},
texture::{DefaultImageSampler, GpuImage},
};
use bevy::ui_render::PreparedUiMaterial;
use bevy_cef_core::prelude::{Browsers, RetainedIoSurface, WebviewGpuSurface};
const REBIND_FRAMES: u8 = 2;
pub(crate) trait WebviewSurfaceSlot: bevy::pbr::Material {
fn webview_surface_slot(&self) -> &Option<Handle<Image>>;
fn webview_surface_slot_mut(&mut self) -> &mut Option<Handle<Image>>;
}
#[derive(SystemSet, Clone, Debug, Hash, PartialEq, Eq)]
pub(crate) enum WebviewSurfaceSet {
Allocate,
Collect,
MarkChanged,
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
struct WebviewBlitLabel;
pub struct WebviewGpuInjectPlugin;
impl Plugin for WebviewGpuInjectPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<PendingWebviewIoSurfaces>()
.configure_sets(
Update,
(
WebviewSurfaceSet::Allocate,
WebviewSurfaceSet::Collect,
WebviewSurfaceSet::MarkChanged,
)
.chain(),
)
.add_systems(
Update,
(
allocate_webview_surfaces_for::<WebviewExtendStandardMaterial>,
allocate_ui_webview_surfaces,
allocate_sprite_webview_surfaces,
allocate_target_webview_surfaces,
)
.in_set(WebviewSurfaceSet::Allocate),
)
.add_systems(
Update,
collect_webview_iosurfaces.in_set(WebviewSurfaceSet::Collect),
)
.add_systems(
Update,
(
mark_webview_materials_changed_for::<WebviewExtendStandardMaterial>,
mark_webview_ui_materials_changed,
mark_sprite_webview_images_changed,
mark_target_webview_images_changed,
)
.in_set(WebviewSurfaceSet::MarkChanged),
);
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
warn!("[macos-gpu-osr] RenderApp sub-app missing; GPU texture injection disabled");
return;
};
render_app
.init_resource::<ExtractedWebviewIoSurfaces>()
.init_resource::<WebviewGpuSurfaces>()
.init_resource::<LiveWebviewSurfaceIds>()
.add_systems(
ExtractSchedule,
(extract_webview_iosurfaces, extract_live_webview_surface_ids),
)
.add_systems(
Render,
inject_webview_gpu_images
.in_set(RenderSystems::PrepareAssets)
.in_set(WebviewGpuImageInjectSet)
.after(prepare_assets::<GpuImage>)
.before(prepare_erased_assets::<MeshMaterial3d<WebviewExtendStandardMaterial>>)
.before(prepare_assets::<PreparedUiMaterial<WebviewUiMaterial>>),
);
let mut render_graph = render_app.world_mut().resource_mut::<RenderGraph>();
render_graph.add_node(WebviewBlitLabel, WebviewBlitNode);
render_graph.add_node_edge(WebviewBlitLabel, bevy::render::graph::CameraDriverLabel);
}
}
struct PendingIoSurface {
id: AssetId<Image>,
surface: RetainedIoSurface,
}
#[derive(Resource, Default)]
struct PendingWebviewIoSurfaces(std::sync::Mutex<Vec<PendingIoSurface>>);
#[derive(Component)]
pub(crate) struct WebviewSurfaceRebind {
frames_left: u8,
}
#[derive(Component)]
struct CollectedSurfaceId(AssetId<Image>);
#[derive(Resource, Default)]
struct ExtractedWebviewIoSurfaces(Vec<PendingIoSurface>);
#[derive(Resource, Default)]
struct WebviewGpuSurfaces(HashMap<AssetId<Image>, WebviewGpuSurface>);
#[derive(Resource, Default)]
struct LiveWebviewSurfaceIds(HashSet<AssetId<Image>>);
fn placeholder_surface_image(size: UVec2) -> Image {
Image::new_fill(
Extent3d {
width: size.x.max(1),
height: size.y.max(1),
depth_or_array_layers: 1,
},
bevy::render::render_resource::TextureDimension::D2,
&[0, 0, 0, 255],
TextureFormat::Bgra8UnormSrgb,
RenderAssetUsages::all(),
)
}
pub(crate) fn allocate_webview_surfaces_for<M: WebviewSurfaceSlot>(
mut commands: Commands,
mut images: ResMut<Assets<Image>>,
mut materials: ResMut<Assets<M>>,
webviews: Query<
(Entity, &MeshMaterial3d<M>, Option<&WebviewSurface>),
(With<WebviewSource>, Without<WebviewTextureTarget>),
>,
) {
for (entity, material_handle, existing) in webviews.iter() {
let Some(material) = materials.get(material_handle.id()) else {
continue;
};
match (material.webview_surface_slot().clone(), existing) {
(Some(handle), Some(surface)) if surface.0.id() == handle.id() => {}
(Some(handle), _) => {
commands.entity(entity).try_insert(WebviewSurface(handle));
}
(None, Some(surface)) => {
if let Some(material) = materials.get_mut(material_handle.id()) {
*material.webview_surface_slot_mut() = Some(surface.0.clone());
}
}
(None, None) => {
let handle = images.add(placeholder_surface_image(UVec2::ONE));
if let Some(material) = materials.get_mut(material_handle.id()) {
*material.webview_surface_slot_mut() = Some(handle.clone());
}
commands.entity(entity).try_insert(WebviewSurface(handle));
}
}
}
}
fn allocate_target_webview_surfaces(
mut commands: Commands,
mut images: ResMut<Assets<Image>>,
webviews: Query<(Entity, &WebviewTextureTarget, Option<&WebviewSurface>), With<WebviewSource>>,
changed: Query<(), Changed<WebviewTextureTarget>>,
mut warned: Local<HashSet<AssetId<Image>>>,
) {
for (entity, target, existing) in webviews.iter() {
if target.0 == Handle::default() {
bevy::log::warn_once!(
"[bevy_cef] WebviewTextureTarget holds Handle::default(); create a \
dedicated image with `images.add(Image::default())` instead"
);
continue;
}
let id = target.0.id();
if existing.is_none_or(|surface| surface.0.id() != id) {
if let Err(err) = images.insert(id, placeholder_surface_image(UVec2::ONE)) {
if warned.insert(id) {
warn!(
"[bevy_cef] WebviewTextureTarget handle is stale; surface not \
allocated for {entity}: {err}"
);
}
continue;
}
commands
.entity(entity)
.try_insert(WebviewSurface(target.0.clone()));
}
}
if !changed.is_empty() {
let mut seen: HashMap<AssetId<Image>, Entity> = HashMap::default();
for (entity, target, _) in webviews.iter() {
if target.0 == Handle::default() {
continue;
}
if let Some(first) = seen.insert(target.0.id(), entity)
&& warned.insert(target.0.id())
{
warn!(
"[bevy_cef] WebviewTextureTarget handle shared by {first} and \
{entity}; only one webview's frames will be visible (last blit wins)"
);
}
}
}
}
fn collect_webview_iosurfaces(
mut commands: Commands,
mut rebinds: Query<(Entity, &mut WebviewSurfaceRebind)>,
mut webviews: Query<(
Entity,
&WebviewSurface,
Option<&mut WebviewIoSurface>,
Option<&mut CollectedSurfaceId>,
)>,
browsers: NonSend<Browsers>,
pending: ResMut<PendingWebviewIoSurfaces>,
) {
for (entity, mut rebind) in rebinds.iter_mut() {
rebind.frames_left = rebind.frames_left.saturating_sub(1);
if rebind.frames_left == 0 {
commands.entity(entity).try_remove::<WebviewSurfaceRebind>();
}
}
let Ok(mut pending) = pending.0.lock() else {
return;
};
pending.clear();
let mut new_frames: HashMap<Entity, RetainedIoSurface> = browsers
.take_latest_webview_iosurfaces(|entity| webviews.contains(entity))
.into_iter()
.collect();
for (entity, surface, io_surface, collected_id) in webviews.iter_mut() {
let id = surface.0.id();
if let Some(retained) = new_frames.remove(&entity) {
let needs_rebind = if let Some(mut io_surface) = io_surface {
let resized =
io_surface.0.width != retained.width || io_surface.0.height != retained.height;
io_surface.0 = retained.clone();
resized
} else {
commands
.entity(entity)
.try_insert(WebviewIoSurface(retained.clone()));
true
};
let rekeyed = if let Some(mut collected_id) = collected_id {
let rekeyed = collected_id.0 != id;
collected_id.0 = id;
rekeyed
} else {
commands.entity(entity).try_insert(CollectedSurfaceId(id));
true
};
if needs_rebind || rekeyed {
commands.entity(entity).try_insert(WebviewSurfaceRebind {
frames_left: REBIND_FRAMES,
});
}
pending.push(PendingIoSurface {
id,
surface: retained,
});
} else if let (Some(io_surface), Some(mut collected_id)) = (io_surface, collected_id) {
if collected_id.0 != id {
collected_id.0 = id;
commands.entity(entity).try_insert(WebviewSurfaceRebind {
frames_left: REBIND_FRAMES,
});
pending.push(PendingIoSurface {
id,
surface: io_surface.0.clone(),
});
}
}
}
}
fn extract_webview_iosurfaces(
mut extracted: ResMut<ExtractedWebviewIoSurfaces>,
pending: Extract<Res<PendingWebviewIoSurfaces>>,
) {
extracted.0.clear();
if let Ok(mut pending) = pending.0.lock() {
extracted.0.append(&mut pending);
}
}
fn extract_live_webview_surface_ids(
mut live: ResMut<LiveWebviewSurfaceIds>,
surfaces: Extract<Query<&WebviewSurface>>,
) {
live.0.clear();
live.0.extend(surfaces.iter().map(|s| s.0.id()));
}
struct WebviewBlitNode;
impl Node for WebviewBlitNode {
fn run<'w>(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
world: &'w World,
) -> Result<(), NodeRunError> {
let extracted = world.resource::<ExtractedWebviewIoSurfaces>();
if extracted.0.is_empty() {
return Ok(());
}
let surfaces = world.resource::<WebviewGpuSurfaces>();
let render_device = render_context.render_device().clone();
let encoder = render_context.command_encoder();
for entry in &extracted.0 {
let Some(surface) = surfaces.0.get(&entry.id) else {
continue;
};
if !surface.import_and_blit(&render_device, encoder, &entry.surface) {
bevy::log::error_once!(
"[macos-gpu-osr] IOSurface import failed ({}x{}); webview textures will \
not update (the macOS GPU OSR path requires the Metal wgpu backend)",
entry.surface.width,
entry.surface.height
);
}
}
Ok(())
}
}
fn inject_webview_gpu_images(
mut surfaces: ResMut<WebviewGpuSurfaces>,
mut gpu_images: ResMut<RenderAssets<GpuImage>>,
extracted: Res<ExtractedWebviewIoSurfaces>,
render_device: Res<RenderDevice>,
default_sampler: Res<DefaultImageSampler>,
live: Res<LiveWebviewSurfaceIds>,
) {
surfaces.0.retain(|id, _| live.0.contains(id));
for entry in &extracted.0 {
if !live.0.contains(&entry.id) {
continue;
}
surfaces
.0
.entry(entry.id)
.or_insert_with(|| {
WebviewGpuSurface::new(&render_device, entry.surface.width, entry.surface.height)
})
.ensure_size(&render_device, entry.surface.width, entry.surface.height);
}
if surfaces.0.is_empty() {
return;
}
let sampler = (**default_sampler).clone();
for (id, surface) in surfaces.0.iter() {
let gpu_image = GpuImage {
texture: surface.texture.clone(),
texture_view: surface.view.clone(),
texture_format: TextureFormat::Bgra8UnormSrgb,
texture_view_format: None,
sampler: sampler.clone(),
size: surface.texture.size(),
mip_level_count: 1,
had_data: true,
};
gpu_images.insert(*id, gpu_image);
}
}
pub(crate) fn mark_webview_materials_changed_for<M: WebviewSurfaceSlot>(
mut materials: ResMut<Assets<M>>,
webviews: Query<&MeshMaterial3d<M>, (With<WebviewSource>, With<WebviewSurfaceRebind>)>,
) {
for handle in webviews.iter() {
let _ = materials.get_mut(handle.id());
}
}
fn allocate_ui_webview_surfaces(
mut commands: Commands,
mut images: ResMut<Assets<Image>>,
mut materials: ResMut<Assets<WebviewUiMaterial>>,
webviews: Query<
(
Entity,
&MaterialNode<WebviewUiMaterial>,
Option<&WebviewSurface>,
),
(With<WebviewSource>, Without<WebviewTextureTarget>),
>,
) {
for (entity, material_handle, existing) in webviews.iter() {
let Some(material) = materials.get(material_handle.id()) else {
continue;
};
match (material.surface.clone(), existing) {
(Some(handle), Some(surface)) if surface.0.id() == handle.id() => {}
(Some(handle), _) => {
commands.entity(entity).try_insert(WebviewSurface(handle));
}
(None, Some(surface)) => {
if let Some(material) = materials.get_mut(material_handle.id()) {
material.surface = Some(surface.0.clone());
}
}
(None, None) => {
let handle = images.add(placeholder_surface_image(UVec2::ONE));
if let Some(material) = materials.get_mut(material_handle.id()) {
material.surface = Some(handle.clone());
}
commands.entity(entity).try_insert(WebviewSurface(handle));
}
}
}
}
fn mark_webview_ui_materials_changed(
webviews: Query<
&MaterialNode<WebviewUiMaterial>,
(With<WebviewSource>, With<WebviewSurfaceRebind>),
>,
mut materials: ResMut<Assets<WebviewUiMaterial>>,
) {
for handle in webviews.iter() {
let _ = materials.get_mut(handle.id());
}
}
fn allocate_sprite_webview_surfaces(
mut commands: Commands,
mut images: ResMut<Assets<Image>>,
mut webviews: Query<
(Entity, &mut Sprite, &WebviewSize),
(
With<WebviewSource>,
Without<WebviewSurface>,
Without<WebviewTextureTarget>,
),
>,
) {
for (entity, mut sprite, size) in webviews.iter_mut() {
let placeholder = placeholder_surface_image(size.0.as_uvec2());
let handle = if sprite.image != Handle::default() && images.get(sprite.image.id()).is_some()
{
let _ = images.insert(sprite.image.id(), placeholder);
sprite.image.clone()
} else {
let handle = images.add(placeholder);
sprite.image = handle.clone();
handle
};
commands.entity(entity).try_insert(WebviewSurface(handle));
}
}
fn mark_sprite_webview_images_changed(
webviews: Query<
&WebviewSurface,
(
With<WebviewSource>,
With<Sprite>,
With<WebviewSurfaceRebind>,
),
>,
mut images: ResMut<Assets<Image>>,
) {
for surface in webviews.iter() {
let _ = images.get_mut(surface.0.id());
}
}
fn mark_target_webview_images_changed(
webviews: Query<
&WebviewSurface,
(
With<WebviewSource>,
With<WebviewTextureTarget>,
With<WebviewSurfaceRebind>,
),
>,
mut images: ResMut<Assets<Image>>,
) {
for surface in webviews.iter() {
let _ = images.get_mut(surface.0.id());
}
}
pub(crate) fn mark_target_materials_changed_for<M: WebviewTextureSlot>(
rebinding: Query<&WebviewSurface, (With<WebviewTextureTarget>, With<WebviewSurfaceRebind>)>,
mut materials: ResMut<Assets<M>>,
) {
let rebind_ids: HashSet<AssetId<Image>> =
rebinding.iter().map(|surface| surface.0.id()).collect();
if rebind_ids.is_empty() {
return;
}
let to_touch: Vec<AssetId<M>> = materials
.iter()
.filter(|(_, material)| {
material
.webview_targets()
.any(|target| rebind_ids.contains(&target))
})
.map(|(id, _)| id)
.collect();
for id in to_touch {
let _ = materials.get_mut(id);
}
}