Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though

This commit is contained in:
Ryan Ward
2026-05-07 11:29:34 -05:00
parent f4e2475c60
commit a41cbd8cc7
2433 changed files with 14208 additions and 9409 deletions
@@ -0,0 +1,87 @@
//! Module containing types to report GPU information that can be useful debug purposes.
use std::fmt::{Display, Formatter};
/// Function called when a GPU device is first selected upon constructing a window.
pub type OnGPUDeviceSelected = dyn Fn(GPUDeviceInfo) + 'static + Send + Sync;
/// Physical GPU device types.
/// This is a direct fork of wgpu's `DeviceType` struct. However, we redefine it to avoid a direct
/// dependency on wgpu in cases where we don't rely on the wgpu rendering backend.
///
/// See <https://docs.rs/wgpu/latest/wgpu/enum.DeviceType.html> for more details.
#[derive(Debug, Copy, Clone)]
pub enum GPUDeviceType {
/// Other or Unknown.
Other,
/// Integrated GPU with shared CPU/GPU memory.
IntegratedGpu,
/// Discrete GPU with separate CPU/GPU memory.
DiscreteGpu,
/// Virtual / Hosted.
VirtualGpu,
/// Cpu / Software Rendering.
Cpu,
}
/// The GPU backend that is being renderer to.
/// This is a direct fork of wgpu's `Backend` struct. However, we redefine it to avoid a direct
/// dependency on wgpu in cases where we don't rely on the wgpu rendering backend.
///
/// See <https://docs.rs/wgpu/latest/wgpu/enum.Backend.html> for more details.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum GPUBackend {
/// Dummy backend, used for testing.
Empty,
/// Vulkan API
Vulkan,
/// Metal API (Apple platforms)
Metal,
/// Direct3D-12 (Windows)
Dx12,
/// OpenGL ES-3 (Linux, Android)
Gl,
/// WebGPU in the browser
BrowserWebGpu,
}
impl Display for GPUBackend {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
GPUBackend::Empty => write!(f, "Empty"),
GPUBackend::Vulkan => write!(f, "Vulkan"),
GPUBackend::Metal => write!(f, "Metal"),
GPUBackend::Dx12 => write!(f, "Dx12"),
GPUBackend::Gl => write!(f, "Gl"),
GPUBackend::BrowserWebGpu => write!(f, "BrowserWebGpu"),
}
}
}
/// Information about the GPU device a given window is rendering to.
#[derive(Debug)]
pub struct GPUDeviceInfo {
/// The type of the device we are rendering to (e.g. integrated vs discrete).
pub device_type: GPUDeviceType,
/// The name of the GPU _device_ we are rendering to.
pub device_name: String,
/// The name of the GPU _driver_ that the OS is using to connect to the given GPU device.
pub driver_name: String,
/// Any additional information about the driver that the OS is using to connect to the given
/// GPU device.
pub driver_info: String,
/// The backend (e.g. Metal vs Vulkan vs OpenGL) we using when rendering.
pub backend: GPUBackend,
}
impl Display for GPUDeviceType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
GPUDeviceType::Other => write!(f, "Other"),
GPUDeviceType::IntegratedGpu => write!(f, "Integrated"),
GPUDeviceType::DiscreteGpu => write!(f, "Discrete"),
GPUDeviceType::VirtualGpu => write!(f, "Virtual"),
GPUDeviceType::Cpu => write!(f, "Cpu"),
}
}
}
+116
View File
@@ -0,0 +1,116 @@
mod gpu_info;
pub mod texture_cache;
pub use gpu_info::{GPUBackend, GPUDeviceInfo, GPUDeviceType, OnGPUDeviceSelected};
use serde::{Deserialize, Serialize};
use crate::platform::GraphicsBackend;
/// Circumstances under which glyphs should be rasterized with thin strokes.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema_gen", derive(schemars::JsonSchema))]
#[cfg_attr(
feature = "schema_gen",
schemars(
description = "When to render text with thinner strokes for a lighter appearance.",
rename_all = "snake_case"
)
)]
#[cfg_attr(feature = "settings_value", derive(settings_value::SettingsValue))]
pub enum ThinStrokes {
/// Never render glyphs using thin strokes.
Never,
/// Render glyphs using thin strokes when rendering on a low-DPI display.
OnLowDpiDisplays,
/// Render glyphs using thin strokes when rendering on a high-DPI display.
#[default]
OnHighDpiDisplays,
/// Always render glyphs using thin strokes.
Always,
}
impl ThinStrokes {
/// The minimum scale factor for which we'll consider a display to be high-DPI.
const HIGH_DPI_SCALE_FACTOR: f32 = 1.5;
pub fn enabled_for_scale_factor(&self, scale_factor: f32) -> bool {
match self {
Self::Never => false,
Self::OnLowDpiDisplays => scale_factor < Self::HIGH_DPI_SCALE_FACTOR,
Self::OnHighDpiDisplays => scale_factor >= Self::HIGH_DPI_SCALE_FACTOR,
Self::Always => true,
}
}
}
/// Options for configuring rendering of glyphs.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct GlyphConfig {
/// Whether to render glyphs using thin strokes.
pub use_thin_strokes: ThinStrokes,
}
/// Power preference for GPU for rendering.
///
/// Relevant for machines with multiple GPUs (typically a discrete high-performance GPU and an
/// integrated low-power-usage GPU).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum GPUPowerPreference {
LowPower,
#[default]
HighPerformance,
}
/// Options for configuring rendering at the application level. These options
/// will apply for the entirety of a frame, but may change between frames.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Config {
/// Configuration options relating to glyph rendering.
pub glyphs: GlyphConfig,
/// Power preference for GPU used for rendering; this is applicable on dual GPU machines where
/// there's a choice between a discrete high-performance GPU and a more power-efficient
/// integrated GPU.
pub gpu_power_preference: GPUPowerPreference,
pub backend_preference: Option<GraphicsBackend>,
}
#[derive(Clone, Debug, Default)]
pub struct CornerRadius {
pub top_left: f32,
pub top_right: f32,
pub bottom_left: f32,
pub bottom_right: f32,
}
impl CornerRadius {
pub fn from_ui_corner_radius(
corner_radius: crate::scene::CornerRadius,
scale_factor: f32,
min_dimension: f32,
) -> Self {
let top_left = match corner_radius.get_top_left() {
crate::scene::Radius::Pixels(px) => px * scale_factor,
crate::scene::Radius::Percentage(percent) => percent / 100. * min_dimension,
};
let top_right = match corner_radius.get_top_right() {
crate::scene::Radius::Pixels(px) => px * scale_factor,
crate::scene::Radius::Percentage(percent) => percent / 100. * min_dimension,
};
let bottom_left = match corner_radius.get_bottom_left() {
crate::scene::Radius::Pixels(px) => px * scale_factor,
crate::scene::Radius::Percentage(percent) => percent / 100. * min_dimension,
};
let bottom_right = match corner_radius.get_bottom_right() {
crate::scene::Radius::Pixels(px) => px * scale_factor,
crate::scene::Radius::Percentage(percent) => percent / 100. * min_dimension,
};
Self {
top_left,
top_right,
bottom_left,
bottom_right,
}
}
}
@@ -0,0 +1,104 @@
use std::sync::{Arc, Weak};
use crate::image_cache::StaticImage;
/// An opaque identifier for a texture from which we can render an image.
///
/// This *MUST* only be used within a single frame, and is not safe to use
/// across frames.
#[derive(Copy, Clone)]
pub struct TextureCacheIndex(usize);
pub struct TextureInfo<T> {
/// The actual information about the texture.
inner: T,
/// The backing asset for the texture.
asset: Weak<StaticImage>,
/// The index of the last frame on which this texture was accessed. This
/// is used to know when a texture has gone "stale" and can be dropped from
/// the cache.
last_accessed_frame: usize,
}
/// A simple cache for textures from which we can render images.
pub struct TextureCache<T> {
textures: Vec<TextureInfo<T>>,
/// The index of the last frame that was rendered.
frame_index: usize,
}
impl<T> TextureCache<T> {
/// The maximum number of frames that a texture can go unused before it
/// gets dropped from the cache.
const MAX_UNUSED_FRAMES: usize = 10;
pub fn new() -> Self {
Self {
textures: Default::default(),
frame_index: 0,
}
}
pub fn get(&self, texture_id: TextureCacheIndex) -> Option<&T> {
self.textures.get(texture_id.0).map(|info| &info.inner)
}
pub fn get_or_insert_by_asset(
&mut self,
asset: &Arc<StaticImage>,
texinfo_func: impl FnOnce(&Arc<StaticImage>) -> T,
) -> (TextureCacheIndex, &T) {
let mut found = None;
let weak_asset = Arc::downgrade(asset);
for (index, texture) in self.textures.iter().enumerate() {
if texture.asset.ptr_eq(&weak_asset) {
found = Some(index);
}
}
let index = match found {
Some(index) => index,
None => {
self.textures.push(TextureInfo {
inner: texinfo_func(asset),
asset: weak_asset.clone(),
last_accessed_frame: self.frame_index,
});
self.textures.len() - 1
}
};
// This array lookup is safe, as we either found the texture in the
// cache or we inserted a new one and returned its index.
self.textures[index].last_accessed_frame = self.frame_index;
(TextureCacheIndex(index), &self.textures[index].inner)
}
/// Updates the texture cache at the end of a frame.
///
/// This should be called at the end of every frame to ensure that stale
/// texture resources get cleaned up.
pub fn end_frame(&mut self) {
// Drop any textures which are no longer referenced by the asset cache
// or have not been rendered in the last MAX_UNUSED_FRAMES frames.
self.textures.retain(|texture| {
texture.asset.strong_count() > 0
&& self.frame_index - texture.last_accessed_frame < Self::MAX_UNUSED_FRAMES
});
self.frame_index += 1;
}
}
impl<T> Default for TextureCache<T> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[path = "texture_cache_tests.rs"]
mod tests;
@@ -0,0 +1,92 @@
use super::*;
use crate::image_cache::test_utils::make_static_image;
#[test]
fn test_end_frame_evicts_when_asset_dropped() {
let mut cache = TextureCache::<()>::new();
let asset = make_static_image(4, 4);
let weak = Arc::downgrade(&asset);
cache.get_or_insert_by_asset(&asset, |_| ());
// Dropping the only strong reference makes strong_count == 0.
drop(asset);
assert_eq!(weak.strong_count(), 0);
// end_frame should detect strong_count == 0 and evict the entry.
cache.end_frame();
assert_eq!(
cache.textures.len(),
0,
"TextureCache should evict entries whose backing asset has been dropped (cascade invariant)"
);
}
#[test]
fn test_end_frame_retains_asset_in_use() {
let mut cache = TextureCache::<()>::new();
let asset = make_static_image(4, 4);
cache.get_or_insert_by_asset(&asset, |_| ());
// The Arc is still alive; the texture should be retained.
cache.end_frame();
assert_eq!(
cache.textures.len(),
1,
"TextureCache should retain entries whose backing asset is still alive"
);
drop(asset);
}
#[test]
fn test_end_frame_evicts_after_max_unused_frames() {
let mut cache = TextureCache::<()>::new();
let asset = make_static_image(4, 4);
cache.get_or_insert_by_asset(&asset, |_| ());
// Advance past MAX_UNUSED_FRAMES without re-accessing the texture.
// The asset is still alive (strong_count > 0), but the texture goes stale.
for _ in 0..TextureCache::<()>::MAX_UNUSED_FRAMES {
cache.end_frame();
assert_eq!(
cache.textures.len(),
1,
"Texture should still be present before MAX_UNUSED_FRAMES is exceeded"
);
}
// One more end_frame tips it over the threshold.
cache.end_frame();
assert_eq!(
cache.textures.len(),
0,
"TextureCache should evict stale entries after MAX_UNUSED_FRAMES unused frames"
);
drop(asset);
}
#[test]
fn test_end_frame_retains_recently_used_entry() {
let mut cache = TextureCache::<()>::new();
let asset = make_static_image(4, 4);
cache.get_or_insert_by_asset(&asset, |_| ());
// Re-access the texture each frame to keep it fresh.
for _ in 0..=TextureCache::<()>::MAX_UNUSED_FRAMES {
cache.get_or_insert_by_asset(&asset, |_| ());
cache.end_frame();
}
assert_eq!(
cache.textures.len(),
1,
"Texture accessed every frame should never be evicted by the frame-count check"
);
drop(asset);
}