use crate::audio::AudioSystem;
#[cfg(feature = "model-3d")]
use crate::context_3d::{Model3dRegistry, Model3dRuntime};
use crate::drawable::DrawCommand;
use crate::graphics::core::Graphics;
use crate::image_shader::ImageShaderDesc;
use crate::input::InputManager;
use crate::pt::Pt;
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::mpsc::{self, Receiver, Sender};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RenderTargetReadbackId(u64);
#[derive(Debug, Clone)]
pub struct RenderTargetPixels {
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) rgba: Vec<u8>,
}
impl RenderTargetPixels {
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn rgba(&self) -> &[u8] {
&self.rgba
}
pub fn into_rgba(self) -> Vec<u8> {
self.rgba
}
}
#[derive(Debug)]
pub(crate) struct RenderTargetReadbackRequest {
pub(crate) id: RenderTargetReadbackId,
pub(crate) texture_id: u32,
pub(crate) width: u32,
pub(crate) height: u32,
}
#[derive(Default)]
struct ResourceMap {
inner: HashMap<TypeId, Rc<dyn Any>>,
}
impl std::fmt::Debug for ResourceMap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ResourceMap")
.field("len", &self.inner.len())
.finish()
}
}
#[derive(Debug)]
pub(crate) struct ContextRuntime {
pub(crate) draw_list: Vec<DrawCommand>,
#[cfg(feature = "model-3d")]
pub(crate) model_3d: Model3dRuntime,
pub(crate) input: InputManager,
pub(crate) scale_factor: f64,
pub(crate) window_logical_size: (Pt, Pt),
pub(crate) graphics: Option<Graphics>,
pub(crate) audio: Option<AudioSystem>,
pub(crate) delta_time: std::time::Duration,
pub(crate) total_elapsed: std::time::Duration,
pub(crate) draw_alpha: f32,
pub(crate) pending_window_title: Option<String>,
pub(crate) pending_cursor_visible: Option<bool>,
pub(crate) pending_fullscreen: Option<bool>,
pub(crate) next_readback_id: u64,
pub(crate) readback_requests: Vec<RenderTargetReadbackRequest>,
pub(crate) readback_sender:
Sender<(RenderTargetReadbackId, Result<RenderTargetPixels, String>)>,
pub(crate) readback_receiver:
Receiver<(RenderTargetReadbackId, Result<RenderTargetPixels, String>)>,
pub(crate) readback_results:
HashMap<RenderTargetReadbackId, Result<RenderTargetPixels, String>>,
}
impl ContextRuntime {
fn new() -> Self {
let (readback_sender, readback_receiver) = mpsc::channel();
Self {
draw_list: Vec::new(),
#[cfg(feature = "model-3d")]
model_3d: Model3dRuntime::default(),
input: InputManager::new(),
scale_factor: 1.0,
window_logical_size: (Pt(0.0), Pt(0.0)),
graphics: None,
audio: None,
delta_time: std::time::Duration::from_secs(0),
total_elapsed: std::time::Duration::from_secs(0),
draw_alpha: 0.0,
pending_window_title: None,
pending_cursor_visible: None,
pending_fullscreen: None,
next_readback_id: 1,
readback_requests: Vec::new(),
readback_sender,
readback_receiver,
readback_results: HashMap::new(),
}
}
}
#[derive(Debug)]
pub(crate) struct ResourceRegistry {
resources: ResourceMap,
pub(crate) textures: Vec<Option<crate::graphics::texture::TextureEntry>>,
pub(crate) images: Vec<Option<crate::image::ImageEntry>>,
#[cfg(feature = "model-3d")]
pub(crate) model_3d: Model3dRegistry,
pub(crate) fonts: HashMap<u32, Vec<u8>>,
pub(crate) image_shaders: HashMap<u32, ImageShaderDesc>,
pub(crate) next_texture_id: u32,
pub(crate) next_image_id: u32,
pub(crate) next_font_id: u32,
pub(crate) next_image_shader_id: u32,
pub(crate) gpu_generation: u32,
pub(crate) dirty_assets: bool,
}
impl ResourceRegistry {
fn new() -> Self {
Self {
resources: ResourceMap::default(),
textures: Vec::new(),
images: Vec::new(),
#[cfg(feature = "model-3d")]
model_3d: Model3dRegistry::default(),
fonts: HashMap::new(),
image_shaders: HashMap::new(),
next_texture_id: 1,
next_image_id: 1,
next_font_id: 1,
next_image_shader_id: 1,
gpu_generation: 1,
dirty_assets: true,
}
}
}
#[derive(Debug)]
pub struct Context {
pub(crate) runtime: ContextRuntime,
pub(crate) registry: ResourceRegistry,
}
impl Context {
pub(crate) fn new() -> Self {
let mut ctx = Self {
runtime: ContextRuntime::new(),
registry: ResourceRegistry::new(),
};
ctx.register_defaults();
ctx
}
pub fn app_data_dir(&self) -> Option<std::path::PathBuf> {
platform_app_data_dir()
}
pub(crate) fn set_delta_time(&mut self, dt: std::time::Duration) {
self.runtime.delta_time = dt;
self.runtime.total_elapsed = self.runtime.total_elapsed.saturating_add(dt);
}
pub(crate) fn set_draw_alpha(&mut self, alpha: f32) {
self.runtime.draw_alpha = alpha;
}
pub(crate) fn delta_time(&self) -> std::time::Duration {
self.runtime.delta_time
}
pub(crate) fn total_elapsed(&self) -> std::time::Duration {
self.runtime.total_elapsed
}
pub fn draw_interpolation(&self) -> f32 {
self.runtime.draw_alpha
}
pub(crate) fn with_audio<R>(&mut self, f: impl FnOnce(&mut AudioSystem) -> R) -> Option<R> {
self.runtime.audio.as_mut().map(f)
}
fn register_defaults(&mut self) {
self.register_image(1, 1, Pt::from(1.0), Pt::from(1.0), &[255, 255, 255, 255]); self.register_image(1, 1, Pt::from(1.0), Pt::from(1.0), &[0, 0, 0, 255]); #[cfg(feature = "model-3d")]
self.register_image(1, 1, Pt::from(1.0), Pt::from(1.0), &[128, 128, 255, 255]);
let text_shader_src = include_str!("shaders/text_tint.wgsl");
self.register_image_shader_desc(ImageShaderDesc::from_wgsl(text_shader_src));
}
pub(crate) fn set_window_logical_size(&mut self, width: Pt, height: Pt) {
let w = Pt(width.0.max(0.0));
let h = Pt(height.0.max(0.0));
self.runtime.window_logical_size = (w, h);
}
pub(crate) fn update_window_metrics_physical(
&mut self,
width: u32,
height: u32,
scale_factor: f64,
) {
self.set_scale_factor(scale_factor);
self.set_window_logical_size(
Pt::from_physical_px(width as f64, scale_factor),
Pt::from_physical_px(height as f64, scale_factor),
);
}
pub(crate) fn set_window_title(&mut self, title: impl Into<String>) {
self.runtime.pending_window_title = Some(title.into());
}
pub(crate) fn set_cursor_visible(&mut self, visible: bool) {
self.runtime.pending_cursor_visible = Some(visible);
}
pub(crate) fn set_fullscreen(&mut self, enabled: bool) {
self.runtime.pending_fullscreen = Some(enabled);
}
pub(crate) fn take_window_title_request(&mut self) -> Option<String> {
self.runtime.pending_window_title.take()
}
pub(crate) fn take_cursor_visible_request(&mut self) -> Option<bool> {
self.runtime.pending_cursor_visible.take()
}
pub(crate) fn take_fullscreen_request(&mut self) -> Option<bool> {
self.runtime.pending_fullscreen.take()
}
pub(crate) fn window_logical_size(&self) -> (Pt, Pt) {
self.runtime.window_logical_size
}
pub(crate) fn vw(&self, percent: f32) -> Pt {
let (w, _) = self.runtime.window_logical_size;
let p = if percent.is_finite() { percent } else { 0.0 };
Pt::from(w.as_f32() * (p / 100.0))
}
pub(crate) fn vh(&self, percent: f32) -> Pt {
let (_, h) = self.runtime.window_logical_size;
let p = if percent.is_finite() { percent } else { 0.0 };
Pt::from(h.as_f32() * (p / 100.0))
}
pub fn insert_resource<T: Any>(&mut self, value: Rc<T>) {
self.registry
.resources
.inner
.insert(TypeId::of::<T>(), value as Rc<dyn Any>);
}
pub(crate) fn get_resource<T: Any>(&self) -> Option<Rc<T>> {
self.registry
.resources
.inner
.get(&TypeId::of::<T>())
.cloned()
.and_then(|v| Rc::downcast::<T>(v).ok())
}
pub fn take_resource<T: Any>(&mut self) -> Option<Rc<T>> {
self.registry
.resources
.inner
.remove(&TypeId::of::<T>())
.and_then(|v| Rc::downcast::<T>(v).ok())
}
pub(crate) fn register_texture(
&mut self,
pixel_width: u32,
pixel_height: u32,
width: Pt,
height: Pt,
rgba: &[u8],
) -> crate::Texture {
let texture_id = self.registry.next_texture_id;
self.registry.next_texture_id += 1;
let image_id = self.registry.next_image_id;
self.registry.next_image_id += 1;
while self.registry.textures.len() <= texture_id as usize {
self.registry.textures.push(None);
}
self.registry.textures[texture_id as usize] =
Some(crate::graphics::texture::TextureEntry::new_sampled(
width,
height,
pixel_width,
pixel_height,
image_id,
std::sync::Arc::from(rgba),
));
let bounds = crate::image::Bounds::new(Pt(0.0), Pt(0.0), width, height);
while self.registry.images.len() <= image_id as usize {
self.registry.images.push(None);
}
self.registry.images[image_id as usize] = Some(crate::image::ImageEntry::new(
texture_id,
bounds,
crate::image::PixelBounds {
x: 0,
y: 0,
width: pixel_width,
height: pixel_height,
},
));
self.registry.dirty_assets = true;
crate::Texture {
id: texture_id,
default_view_id: image_id,
width,
height,
pixel_width,
pixel_height,
}
}
pub(crate) fn register_streaming_texture(
&mut self,
pixel_width: u32,
pixel_height: u32,
width: Pt,
height: Pt,
rgba: &[u8],
) -> crate::Texture {
let texture_id = self.registry.next_texture_id;
self.registry.next_texture_id += 1;
let image_id = self.registry.next_image_id;
self.registry.next_image_id += 1;
while self.registry.textures.len() <= texture_id as usize {
self.registry.textures.push(None);
}
self.registry.textures[texture_id as usize] =
Some(crate::graphics::texture::TextureEntry::new_streaming(
width,
height,
pixel_width,
pixel_height,
image_id,
std::sync::Arc::from(rgba),
));
while self.registry.images.len() <= image_id as usize {
self.registry.images.push(None);
}
self.registry.images[image_id as usize] = Some(crate::image::ImageEntry::new(
texture_id,
crate::image::Bounds::new(Pt(0.0), Pt(0.0), width, height),
crate::image::PixelBounds {
x: 0,
y: 0,
width: pixel_width,
height: pixel_height,
},
));
self.registry.dirty_assets = true;
crate::Texture {
id: texture_id,
default_view_id: image_id,
width,
height,
pixel_width,
pixel_height,
}
}
pub(crate) fn update_texture_rgba8(
&mut self,
texture: crate::Texture,
rgba: Vec<u8>,
) -> anyhow::Result<()> {
let entry = self
.registry
.textures
.get_mut(texture.id as usize)
.and_then(Option::as_mut)
.ok_or_else(|| anyhow::anyhow!("unknown texture id {}", texture.id))?;
if entry.render_target {
anyhow::bail!("cannot stream pixels into a render-target texture");
}
let expected_len = texture.pixel_width as usize * texture.pixel_height as usize * 4;
if rgba.len() != expected_len {
anyhow::bail!(
"RGBA data length mismatch: expected {}, got {}",
expected_len,
rgba.len()
);
}
entry.raw_data = Some(std::sync::Arc::from(rgba.clone()));
if entry.is_ready(self.registry.gpu_generation) {
entry
.pending_uploads
.push(crate::graphics::texture::TextureUploadRegion {
x: 0,
y: 0,
width: texture.pixel_width,
height: texture.pixel_height,
rgba,
});
}
self.registry.dirty_assets = true;
Ok(())
}
pub(crate) fn destroy_texture(&mut self, texture: crate::Texture) -> bool {
let is_dynamic_atlas = self
.registry
.textures
.get(texture.id() as usize)
.and_then(Option::as_ref)
.map(|entry| entry.dynamic_atlas)
.unwrap_or(false);
if is_dynamic_atlas {
return false;
}
let texture_id = texture.id();
let Some(slot) = self.registry.textures.get_mut(texture_id as usize) else {
return false;
};
if slot.take().is_none() {
return false;
}
for image in &mut self.registry.images {
if image
.as_ref()
.is_some_and(|entry| entry.texture_id == texture_id)
{
*image = None;
}
}
self.registry.dirty_assets = true;
true
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
pub(crate) fn queue_external_video_copy(
&mut self,
texture: crate::Texture,
video: web_sys::HtmlVideoElement,
) {
let queue = self
.get_resource::<crate::video::VideoCopyQueue>()
.unwrap_or_else(|| {
let queue = std::rc::Rc::new(crate::video::VideoCopyQueue::default());
self.insert_resource(queue.clone());
queue
});
queue
.copies
.borrow_mut()
.retain(|copy| copy.texture_id != texture.id);
queue
.copies
.borrow_mut()
.push(crate::video::ExternalVideoCopy {
texture_id: texture.id,
width: texture.pixel_width,
height: texture.pixel_height,
video,
});
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
pub(crate) fn take_external_video_copies(&mut self) -> Vec<crate::video::ExternalVideoCopy> {
self.get_resource::<crate::video::VideoCopyQueue>()
.map(|queue| queue.copies.borrow_mut().drain(..).collect())
.unwrap_or_default()
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
pub(crate) fn has_external_video_copies(&self) -> bool {
self.get_resource::<crate::video::VideoCopyQueue>()
.map(|queue| !queue.copies.borrow().is_empty())
.unwrap_or(false)
}
pub fn register_image(
&mut self,
pixel_width: u32,
pixel_height: u32,
width: Pt,
height: Pt,
rgba: &[u8],
) -> crate::Image {
if pixel_width <= 512
&& pixel_height <= 512
&& let Some(graphics) = self.runtime.graphics.as_mut()
&& let Some(atlas) = graphics.shared_atlas.as_mut()
{
let scale_factor = self.runtime.scale_factor;
if let Ok(img) = atlas.add_region(
&mut self.registry,
scale_factor,
width,
height,
pixel_width,
pixel_height,
rgba,
) {
return img;
}
}
self.register_texture(pixel_width, pixel_height, width, height, rgba)
.view()
}
pub(crate) fn register_sub_image(
&mut self,
image: crate::image::Image,
bounds: crate::image::Bounds,
) -> anyhow::Result<u32> {
let id = self.registry.next_image_id;
self.registry.next_image_id += 1;
let physical_w_ratio = image.pixel_bounds.width as f32 / image.width.0.max(1e-5);
let physical_h_ratio = image.pixel_bounds.height as f32 / image.height.0.max(1e-5);
let parent_pixel_bounds = self.registry.images[image.index()]
.as_ref()
.unwrap()
.pixel_bounds;
let parent_pixel_x = parent_pixel_bounds.x;
let parent_pixel_y = parent_pixel_bounds.y;
let pixel_x = parent_pixel_x + (bounds.x.0 * physical_w_ratio).round() as u32;
let pixel_y = parent_pixel_y + (bounds.y.0 * physical_h_ratio).round() as u32;
let pixel_width = (bounds.width.0 * physical_w_ratio).round() as u32;
let pixel_height = (bounds.height.0 * physical_h_ratio).round() as u32;
let entry = crate::image::ImageEntry::new(
image.texture_id(),
crate::image::Bounds::new(
image.x + bounds.x,
image.y + bounds.y,
bounds.width,
bounds.height,
),
crate::image::PixelBounds {
x: pixel_x,
y: pixel_y,
width: pixel_width,
height: pixel_height,
},
);
while self.registry.images.len() <= id as usize {
self.registry.images.push(None);
}
self.registry.images[id as usize] = Some(entry);
self.registry.dirty_assets = true;
Ok(id)
}
pub(crate) fn register_font(&mut self, font_data: Vec<u8>) -> u32 {
let id = self.registry.next_font_id;
self.registry.next_font_id += 1;
self.registry.fonts.insert(id, font_data);
self.registry.dirty_assets = true;
id
}
pub(crate) fn register_image_shader_desc(&mut self, desc: ImageShaderDesc) -> u32 {
let id = self.registry.next_image_shader_id;
self.registry.next_image_shader_id += 1;
self.registry.image_shaders.insert(id, desc);
self.registry.dirty_assets = true;
id
}
pub(crate) fn register_render_target_texture(
&mut self,
width: Pt,
height: Pt,
) -> crate::Texture {
let texture_id = self.registry.next_texture_id;
self.registry.next_texture_id += 1;
let image_id = self.registry.next_image_id;
self.registry.next_image_id += 1;
let pixel_width = width.to_u32_clamped().max(1);
let pixel_height = height.to_u32_clamped().max(1);
while self.registry.textures.len() <= texture_id as usize {
self.registry.textures.push(None);
}
self.registry.textures[texture_id as usize] =
Some(crate::graphics::texture::TextureEntry::new_render_target(
width,
height,
pixel_width,
pixel_height,
image_id,
));
while self.registry.images.len() <= image_id as usize {
self.registry.images.push(None);
}
self.registry.images[image_id as usize] = Some(crate::image::ImageEntry::new(
texture_id,
crate::image::Bounds::new(Pt(0.0), Pt(0.0), width, height),
crate::image::PixelBounds {
x: 0,
y: 0,
width: pixel_width,
height: pixel_height,
},
));
self.registry.dirty_assets = true;
crate::Texture {
id: texture_id,
default_view_id: image_id,
width,
height,
pixel_width,
pixel_height,
}
}
pub(crate) fn insert_resource_dyn(&mut self, type_id: TypeId, value: Rc<dyn Any>) {
self.registry.resources.inner.insert(type_id, value);
}
pub(crate) fn take_resource_dyn(&mut self, type_id: TypeId) {
self.registry.resources.inner.remove(&type_id);
}
pub(crate) fn begin_frame(&mut self) {
self.runtime.draw_list.clear();
#[cfg(feature = "model-3d")]
self.runtime.model_3d.begin_frame();
}
pub(crate) fn clear_transient_state(&mut self) {
self.begin_frame();
self.runtime.pending_window_title = None;
self.runtime.pending_cursor_visible = None;
self.runtime.pending_fullscreen = None;
}
pub(crate) fn clear_transient_input(&mut self) {
self.runtime.input.clear_transient_state();
}
pub(crate) fn input(&self) -> &InputManager {
&self.runtime.input
}
pub(crate) fn input_mut(&mut self) -> &mut InputManager {
&mut self.runtime.input
}
pub(crate) fn set_scale_factor(&mut self, scale_factor: f64) {
self.runtime.scale_factor = scale_factor;
}
pub(crate) fn graphics_mut(&mut self) -> Option<&mut Graphics> {
self.runtime.graphics.as_mut()
}
#[cfg_attr(not(target_os = "android"), allow(dead_code))]
pub(crate) fn has_graphics(&self) -> bool {
self.runtime.graphics.is_some()
}
pub(crate) fn attach_graphics(&mut self, graphics: Graphics) {
self.runtime.graphics = Some(graphics);
}
pub(crate) fn detach_graphics(&mut self) -> Option<Graphics> {
self.runtime.graphics.take()
}
#[cfg_attr(not(target_os = "android"), allow(dead_code))]
pub(crate) fn bump_gpu_generation(&mut self) {
self.registry.gpu_generation = self.registry.gpu_generation.saturating_add(1);
self.registry.dirty_assets = true;
}
pub fn scale_factor(&self) -> f64 {
self.runtime.scale_factor
}
pub fn request_render_target_readback(
&mut self,
target: crate::Image,
) -> Result<RenderTargetReadbackId, String> {
let Some(entry) = self
.registry
.textures
.get(target.texture_id() as usize)
.and_then(|entry| entry.as_ref())
else {
return Err(format!(
"render target texture {} does not exist",
target.texture_id()
));
};
if !entry.is_render_target() {
return Err(format!(
"texture {} is not a render target",
target.texture_id()
));
}
let bounds = target.pixel_bounds();
if bounds.x != 0
|| bounds.y != 0
|| bounds.width != entry.pixel_width
|| bounds.height != entry.pixel_height
{
return Err("render-target readback requires the full target view".to_string());
}
let request_id = RenderTargetReadbackId(self.runtime.next_readback_id);
self.runtime.next_readback_id = self.runtime.next_readback_id.saturating_add(1).max(1);
self.runtime
.readback_requests
.push(RenderTargetReadbackRequest {
id: request_id,
texture_id: target.texture_id(),
width: entry.pixel_width,
height: entry.pixel_height,
});
Ok(request_id)
}
pub fn poll_render_target_readback(
&mut self,
request_id: RenderTargetReadbackId,
) -> Option<Result<RenderTargetPixels, String>> {
if let Some(graphics) = self.runtime.graphics.as_ref() {
let _ = graphics.device.poll(wgpu::PollType::Poll);
}
while let Ok((result_id, result)) = self.runtime.readback_receiver.try_recv() {
self.runtime.readback_results.insert(result_id, result);
}
self.runtime.readback_results.remove(&request_id)
}
pub(crate) fn take_render_target_readback_requests(
&mut self,
) -> Vec<RenderTargetReadbackRequest> {
std::mem::take(&mut self.runtime.readback_requests)
}
pub(crate) fn resolve_target_texture_id(&self, target: crate::Image) -> u32 {
if target.texture_id == 0 {
return 0;
}
let Some(texture_entry) = self
.registry
.textures
.get(target.texture_id as usize)
.and_then(|v| v.as_ref())
else {
panic!(
"[spot][target] image {} points to missing texture {}",
target.id, target.texture_id
);
};
if !texture_entry.is_render_target() {
panic!(
"[spot][target] image {} uses texture {} which is not a render target",
target.id, target.texture_id
);
}
if texture_entry.default_view_id != target.id {
panic!(
"[spot][target] image {} is not the full target view for texture {}",
target.id, target.texture_id
);
}
target.texture_id
}
pub(crate) fn target_logical_size(&self, target_texture_id: u32) -> Option<(Pt, Pt)> {
if target_texture_id == 0 {
return Some(self.runtime.window_logical_size);
}
self.registry
.textures
.get(target_texture_id as usize)
.and_then(|v| v.as_ref())
.map(|entry| (entry.width, entry.height))
}
pub(crate) fn push(&mut self, mut drawable: DrawCommand) {
match &mut drawable {
DrawCommand::Image(_) => {}
DrawCommand::Text(_) => {}
}
if let DrawCommand::Image(cmd) = &drawable {
let id = cmd.id;
let opts = &cmd.opts;
let size = cmd.size;
let pos = opts.position();
let scale = opts.scale();
let rot = opts.rotation();
let w = size[0].as_f32() * scale[0];
let h = size[1].as_f32() * scale[1];
let (vw, vh) = self
.target_logical_size(cmd.target_texture_id)
.unwrap_or(self.runtime.window_logical_size);
let screen_w = vw.as_f32();
let screen_h = vh.as_f32();
let is_visible = if rot == 0.0 {
let x0 = pos[0].as_f32();
let y0 = pos[1].as_f32();
let x1 = x0 + w;
let y1 = y0 + h;
let min_x = x0.min(x1);
let max_x = x0.max(x1);
let min_y = y0.min(y1);
let max_y = y0.max(y1);
!(max_x < 0.0 || min_x > screen_w || max_y < 0.0 || min_y > screen_h)
} else {
let c = rot.cos();
let s = rot.sin();
let x2 = w * c;
let y2 = w * s;
let x3 = -h * s;
let y3 = h * c;
let x4 = x2 + x3;
let y4 = y2 + y3;
let min_x = 0.0f32.min(x2).min(x3).min(x4);
let max_x = 0.0f32.max(x2).max(x3).max(x4);
let min_y = 0.0f32.min(y2).min(y3).min(y4);
let max_y = 0.0f32.max(y2).max(y3).max(y4);
!(pos[0].as_f32() + max_x < 0.0
|| pos[0].as_f32() + min_x > screen_w
|| pos[1].as_f32() + max_y < 0.0
|| pos[1].as_f32() + min_y > screen_h)
};
if !is_visible {
if std::env::var("SPOT_DEBUG_CULL").is_ok() {
eprintln!(
"[spot][cull] image id={} at {:?} (size {:?}) is culled (screen: {:?})",
id,
pos,
[w, h],
self.runtime.window_logical_size
);
}
return;
}
}
if std::env::var("SPOT_DEBUG_DRAW").is_ok() {
match &drawable {
DrawCommand::Image(cmd) => {
eprintln!(
"[spot][debug] draw image id={} target={} shader_id={} pos={:?}",
cmd.id,
cmd.target_texture_id,
cmd.shader_id,
cmd.opts.position(),
);
}
DrawCommand::Text(cmd) => {
eprintln!(
"[spot][debug] draw text target={} pos={:?}",
cmd.target_texture_id,
cmd.opts.position(),
);
}
}
}
self.runtime.draw_list.push(drawable);
}
}
#[cfg(target_os = "android")]
fn platform_app_data_dir() -> Option<std::path::PathBuf> {
crate::android::get_app().and_then(|app| app.internal_data_path())
}
#[cfg(target_os = "ios")]
fn platform_app_data_dir() -> Option<std::path::PathBuf> {
std::env::var_os("HOME").map(|home| {
std::path::PathBuf::from(home)
.join("Library")
.join("Application Support")
})
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn platform_app_data_dir() -> Option<std::path::PathBuf> {
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Pt;
#[test]
fn test_auto_atlas_logic() {
let mut ctx = Context::new();
let img1 = ctx.register_image(1, 1, Pt(10.0), Pt(10.0), &[0, 0, 0, 0]);
let img2 = ctx.register_image(1, 1, Pt(10.0), Pt(10.0), &[0, 0, 0, 0]);
assert_ne!(
img1.texture_id(),
img2.texture_id(),
"Should not auto-atlas without graphics"
);
}
}