use std::collections::{HashMap, HashSet};
#[cfg(not(target_arch = "wasm32"))]
use arboard::{Clipboard, ImageData};
use egui::TextureId;
use gled_gpu::effect_state::{EffectKey, EffectPipelines};
#[cfg(not(target_arch = "wasm32"))]
use kanal::bounded;
#[cfg(not(target_arch = "wasm32"))]
use wgpu::PollType;
use wgpu::{Extent3d, MapMode};
use crate::wgpu_render_state;
use gled_protocol::action::UiAction;
#[derive(Debug, PartialEq, Clone)]
pub(crate) struct OwnedTextureId(pub(crate) TextureId);
impl Drop for OwnedTextureId {
fn drop(&mut self) {
wgpu_render_state().renderer.write().free_texture(&self.0)
}
}
#[derive(Debug, Default)]
pub(crate) struct ThumbnailTextures {
textures: HashMap<EffectKey, (u64, OwnedTextureId)>,
}
impl ThumbnailTextures {
pub(crate) fn sync(&mut self, pipelines: &EffectPipelines) {
let render_state = wgpu_render_state();
let mut seen = HashSet::new();
for (key, thumbnail) in pipelines.thumbnails() {
seen.insert(key);
let current = self
.textures
.get(&key)
.is_some_and(|(generation, _)| *generation == thumbnail.generation);
if !current {
let id = render_state.renderer.write().register_native_texture(
&render_state.device,
thumbnail.view,
wgpu::FilterMode::Nearest,
);
self.textures
.insert(key, (thumbnail.generation, OwnedTextureId(id)));
}
}
self.textures.retain(|key, _| seen.contains(key));
}
pub(crate) fn texture_id(&self, key: &EffectKey) -> Option<TextureId> {
self.textures.get(key).map(|(_, id)| id.0)
}
}
fn read_back(pipelines: &EffectPipelines, key: &EffectKey) -> Option<(wgpu::Buffer, Extent3d)> {
let thumbnail = pipelines.get(key).and_then(|state| state.thumbnail())?;
let render_state = wgpu_render_state();
let device = &render_state.device;
let texture = thumbnail.texture;
let texture_size = texture.size();
let buffer_size = (texture_size.width * texture_size.height * 4) as wgpu::BufferAddress;
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Texture Buffer"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Copy Texture to Buffer"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(texture_size.width * 4),
rows_per_image: Some(texture_size.height),
},
},
texture_size,
);
render_state.queue.submit([encoder.finish()]);
Some((buffer, texture_size))
}
fn to_opaque_rgba(data: &mut [u8]) {
data.as_chunks_mut::<4>().0.iter_mut().for_each(|pixel| {
pixel.swap(0, 2);
pixel[3] = 255;
});
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn copy_thumbnail_to_clipboard(
ctx: &egui::Context,
pipelines: &EffectPipelines,
key: &EffectKey,
) {
let Some((buffer, size)) = read_back(pipelines, key) else {
return;
};
let ctx = ctx.clone();
let mapped = buffer.clone();
buffer.slice(..).map_async(MapMode::Read, move |result| {
let data = result.map_err(|err| err.to_string()).and_then(|()| {
mapped
.slice(..)
.get_mapped_range()
.map(|range| range.to_vec())
.map_err(|err| err.to_string())
});
mapped.unmap();
match data {
Ok(mut data) => {
to_opaque_rgba(&mut data);
ctx.copy_image(egui::ColorImage::from_rgba_unmultiplied(
[size.width as usize, size.height as usize],
&data,
));
}
Err(err) => {
UiAction::Error(format!("Could not read the rendered image: {err}")).enqueue();
}
}
ctx.request_repaint();
});
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn copy_thumbnail_to_clipboard(
_ctx: &egui::Context,
pipelines: &EffectPipelines,
key: &EffectKey,
) {
let Some((buffer, texture_size)) = read_back(pipelines, key) else {
return;
};
let device = &wgpu_render_state().device;
let buffer_slice = buffer.slice(..);
let (tx, rx) = bounded(1);
buffer_slice.map_async(MapMode::Read, move |result| {
let _ = tx.send(result);
});
if let Err(err) = device.poll(PollType::wait_indefinitely()) {
UiAction::Error(format!("Could not read the rendered image: {err}")).enqueue();
return;
}
match rx.recv() {
Ok(Ok(())) => {}
Ok(Err(err)) => {
UiAction::Error(format!("Could not read the rendered image: {err}")).enqueue();
return;
}
Err(err) => {
UiAction::Error(format!("Could not read the rendered image: {err}")).enqueue();
return;
}
}
let mut data = match buffer_slice.get_mapped_range() {
Ok(range) => range.to_vec(),
Err(err) => {
UiAction::Error(format!("Could not read the rendered image: {err}")).enqueue();
return;
}
};
buffer.unmap();
std::thread::Builder::new()
.name("gled:clipboard:copy".to_string())
.spawn(move || {
#[cfg(feature = "profiling")]
profiling::register_thread!("copy_image_to_clipboard");
to_opaque_rgba(&mut data);
if let Ok(mut clipboard) = Clipboard::new() {
if let Err(err) = clipboard.set_image(ImageData {
bytes: data.into(),
width: texture_size.width as usize,
height: texture_size.height as usize,
}) {
UiAction::Error(format!("Could not copy rendered image to clipboard: {err}"))
.enqueue();
} else {
tracing::info!("Copied rendered image to clipboard");
}
}
})
.ok();
}
pub(crate) fn scene_instance_texture_ids(
instance: &gled_model::asset::scene::instance::SceneInstance,
textures: &ThumbnailTextures,
) -> Vec<TextureId> {
gled_gpu::scene_render::scene_effect_keys(instance.id, &instance.scene)
.filter_map(|key| textures.texture_id(&key))
.collect()
}
#[derive(Debug)]
pub(crate) struct UiPreview {
pub(crate) indices: gled_gpu::preview_indices::PreviewIndices,
pub(crate) preview: gled_gpu::preview::Preview,
pub(crate) texture_id: OwnedTextureId,
}
impl UiPreview {
pub(crate) fn new(render_state: &eframe::egui_wgpu::RenderState) -> Self {
let indices = gled_gpu::preview_indices::PreviewIndices::new(&render_state.device);
let preview = gled_gpu::preview::Preview::new(&render_state.device, &indices);
let texture_id = OwnedTextureId(render_state.renderer.write().register_native_texture(
&render_state.device,
preview.view(),
wgpu::FilterMode::Nearest,
));
Self {
indices,
preview,
texture_id,
}
}
}