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,104 @@
use metal::{MTLPixelFormat, MTLStorageMode};
use pathfinder_geometry::vector::Vector2F;
use galaxyui_core::platform::CapturedFrame;
#[cfg(test)]
#[path = "frame_capture_tests.rs"]
mod tests;
/// Captures a rendered frame from a Metal texture and returns the raw BGRA pixel data.
///
/// The data is returned in Metal's native BGRA format to avoid an expensive
/// pixel-format conversion on the render thread. Consumers that need RGBA
/// should call `CapturedFrame::ensure_rgba()`.
///
/// # Arguments
/// * `texture` - The Metal texture containing the rendered frame
/// * `size` - The dimensions of the texture (width, height)
///
/// # Returns
/// * `Some(CapturedFrame)` containing the RGBA pixel data if successful
/// * `None` if the texture dimensions are invalid
pub fn capture_frame(texture: &metal::TextureRef, size: Vector2F) -> Option<CapturedFrame> {
let width = size.x() as usize;
let height = size.y() as usize;
if width == 0 || height == 0 {
log::warn!("Invalid texture dimensions: {}x{}", width, height);
return None;
}
let bytes_per_row = width * 4;
let buffer_size = bytes_per_row * height;
let mut pixel_data: Vec<u8> = vec![0u8; buffer_size];
let region = metal::MTLRegion {
origin: metal::MTLOrigin { x: 0, y: 0, z: 0 },
size: metal::MTLSize {
width: width as u64,
height: height as u64,
depth: 1,
},
};
texture.get_bytes(
pixel_data.as_mut_ptr() as *mut std::ffi::c_void,
bytes_per_row as u64,
region,
0,
);
Some(CapturedFrame::new_bgra(
width as u32,
height as u32,
pixel_data,
))
}
#[cfg(test)]
pub(crate) fn convert_bgra_to_rgba(data: &mut [u8]) {
for chunk in data.chunks_exact_mut(4) {
chunk.swap(0, 2);
}
}
/// Creates an off-screen Metal texture
///
/// This is a utility function for headless/off-screen rendering scenarios where
/// you need to render to a texture rather than a window drawable. Currently unused
/// but kept for future headless capture or visual regression testing support.
///
/// # Arguments
/// * `device` - The Metal device to create the texture on
/// * `width` - The width of the texture in pixels
/// * `height` - The height of the texture in pixels
/// * `pixel_format` - The pixel format (should match the drawable format)
///
/// # Returns
/// * A new Metal texture that can be rendered to and read back from
#[allow(dead_code)]
pub fn create_capture_texture(
device: &metal::Device,
width: u64,
height: u64,
pixel_format: MTLPixelFormat,
) -> metal::Texture {
let texture_descriptor = metal::TextureDescriptor::new();
texture_descriptor.set_pixel_format(pixel_format);
texture_descriptor.set_width(width);
texture_descriptor.set_height(height);
texture_descriptor.set_depth(1);
texture_descriptor.set_mipmap_level_count(1);
texture_descriptor.set_sample_count(1);
texture_descriptor.set_array_length(1);
// Set usage flags for rendering and reading
texture_descriptor
.set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead);
// Use managed storage mode so we can read it back
texture_descriptor.set_storage_mode(MTLStorageMode::Managed);
device.new_texture(&texture_descriptor)
}
@@ -0,0 +1,20 @@
use super::convert_bgra_to_rgba;
#[test]
fn test_convert_bgra_to_rgba() {
let mut data = vec![
0xBB, 0xCC, 0xFF, 0xAA, // BGRA pixel (Blue, Green, Red, Alpha)
0x11, 0x22, 0x33, 0x44, // Another BGRA pixel
];
convert_bgra_to_rgba(&mut data);
// After conversion, should be RGBA (Red, Green, Blue, Alpha)
assert_eq!(
data,
vec![
0xFF, 0xCC, 0xBB, 0xAA, // RGBA pixel
0x33, 0x22, 0x11, 0x44, // Another RGBA pixel
]
);
}
@@ -0,0 +1,13 @@
pub mod frame_capture;
mod renderer;
mod renderer_manager;
pub use renderer_manager::RendererManager;
/// Returns `true` if the given metal Device corresponds to the low power/integrated GPU.
///
/// In dual GPU Macs, this is `false` for the discrete high-performance GPU.
#[cfg_attr(wgpu, allow(dead_code))]
pub fn is_integrated_gpu(device: &metal::Device) -> bool {
device.is_low_power() && !device.is_removable()
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
use crate::platform::mac::rendering::metal::renderer::Renderer;
use std::collections::HashMap;
use galaxyui_core::rendering;
pub struct RendererManager {
/// Maps a device's registry ID to its renderer (collection of state related
/// to rendering on a particular device).
renderers: HashMap<u64, Renderer>,
}
impl RendererManager {
pub fn new() -> Self {
Self {
renderers: Default::default(),
}
}
pub fn renderer_for_device(&mut self, device: &metal::Device) -> &mut Renderer {
use std::collections::hash_map::Entry::*;
match self.renderers.entry(device.registry_id()) {
Occupied(entry) => entry.into_mut(),
Vacant(entry) => entry.insert(Renderer::new(
device,
metal::MTLPixelFormat::BGRA8Unorm,
rendering::GlyphConfig::default(),
)),
}
}
}
@@ -0,0 +1,52 @@
#ifndef shader_types_h
#define shader_types_h
#include <simd/simd.h>
typedef struct {
vector_float2 viewport_size;
} Uniforms;
typedef struct {
vector_float2 origin;
vector_float2 size;
float corner_radius_top_left;
float corner_radius_top_right;
float corner_radius_bottom_left;
float corner_radius_bottom_right;
float border_top;
float border_right;
float border_bottom;
float border_left;
vector_float2 background_start;
vector_float2 background_end;
vector_float4 background_start_color;
vector_float4 background_end_color;
vector_float2 border_start;
vector_float2 border_end;
vector_float4 border_start_color;
vector_float4 border_end_color;
vector_float4 icon_color;
int is_icon;
vector_float2 drop_shadow_offsets;
vector_float4 drop_shadow_color;
float drop_shadow_sigma;
float drop_shadow_padding_factor;
float dash_length;
vector_float2 gap_lengths;
} PerRectUniforms;
typedef struct {
vector_float2 origin;
vector_float2 size;
float uv_left;
float uv_top;
float uv_width;
float uv_height;
float fade_start;
float fade_end;
vector_float4 color;
int is_emoji;
} PerGlyphUniforms;
#endif // shader_types_h
@@ -0,0 +1,402 @@
#include <metal_stdlib>
using namespace metal;
#include "shader_types.h"
constant float EPSILON = 0.00001;
// Vertex shader outputs and fragment shader inputs
struct RectFragmentData
{
float4 position [[position]];
float2 pixel_position [[pixel_position]];
float2 rect_origin;
float2 rect_size;
float2 rect_center;
float2 rect_corner;
float border_top;
float border_right;
float border_bottom;
float border_left;
float corner_radius_top_left;
float corner_radius_top_right;
float corner_radius_bottom_left;
float corner_radius_bottom_right;
float2 background_start;
float2 background_end;
float4 background_start_color;
float4 background_end_color;
float2 border_start;
float2 border_end;
float4 border_start_color;
float4 border_end_color;
float2 texture_coordinate;
bool is_icon;
float4 icon_color;
float2 drop_shadow_offsets;
float4 drop_shadow_color;
float drop_shadow_sigma;
float drop_shadow_padding_factor;
float dash_length;
float2 gap_lengths;
};
struct GlyphFragmentData
{
float4 position [[position]];
float2 rect_center;
float2 rect_corner;
float2 texture_coordinate;
float fade_alpha;
float4 color;
bool is_emoji;
};
float distance_from_rect(vector_float2 pixel_pos, vector_float2 rect_center, vector_float2 rect_corner, float corner_radius) {
vector_float2 p = pixel_pos - rect_center;
vector_float2 q = abs(p) - rect_corner + corner_radius;
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - corner_radius;
}
float4 derive_color(float2 pixel_pos, float2 start, float2 end, float4 start_color, float4 end_color) {
float2 adjusted_end = end - start;
float h = dot(pixel_pos - start, adjusted_end) / dot(adjusted_end, adjusted_end);
return mix(start_color, end_color, h);
}
vertex RectFragmentData
rect_vertex_shader(
uint vertex_id [[vertex_id]],
uint instance_id [[instance_id]],
constant float2 *vertices [[buffer(0)]],
constant PerRectUniforms *glyph_uniforms [[buffer(1)]],
constant Uniforms *uniforms [[buffer(2)]])
{
const constant PerRectUniforms *rect = &glyph_uniforms[instance_id];
float2 pixel_pos = vertices[vertex_id] * rect->size + rect->origin;
float2 device_pos = pixel_pos / uniforms->viewport_size * float2(2.0, -2.0) + float2(-1.0, 1.0);
RectFragmentData out;
out.position = float4(device_pos, 0.0, 1.0);
out.pixel_position = pixel_pos;
out.rect_origin = rect->origin;
out.rect_size = rect->size;
out.rect_corner = rect->size / 2.0;
out.rect_center = rect->origin + out.rect_corner;
out.border_top = rect->border_top;
out.border_right = rect->border_right;
out.border_bottom = rect->border_bottom;
out.border_left = rect->border_left;
out.corner_radius_top_left = rect->corner_radius_top_left;
out.corner_radius_top_right = rect->corner_radius_top_right;
out.corner_radius_bottom_left = rect->corner_radius_bottom_left;
out.corner_radius_bottom_right = rect->corner_radius_bottom_right;
out.background_start = rect->background_start * rect->size + rect->origin;
out.background_end = rect->background_end * rect->size + rect->origin;
out.background_start_color = rect->background_start_color;
out.background_end_color = rect->background_end_color;
out.border_start = rect->border_start * rect->size + rect->origin;
out.border_end = rect->border_end * rect->size + rect->origin;
out.border_start_color = rect->border_start_color;
out.border_end_color = rect->border_end_color;
out.texture_coordinate = vertices[vertex_id];
out.is_icon = rect->is_icon;
out.icon_color = rect->icon_color;
out.drop_shadow_offsets = rect->drop_shadow_offsets;
out.drop_shadow_color = rect->drop_shadow_color;
out.drop_shadow_sigma = rect->drop_shadow_sigma;
out.drop_shadow_padding_factor = rect->drop_shadow_padding_factor;
out.dash_length = rect->dash_length;
out.gap_lengths = rect->gap_lengths;
return out;
}
// Drop shadow code *heavily* inspired by this post:
// http://madebyevan.com/shaders/fast-rounded-rectangle-shadows/
// A standard gaussian function, used for weighting samples
float gaussian(float x, float sigma) {
const float pi = 3.141592653589793;
return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * pi) * sigma);
}
// This approximates the error function, needed for the gaussian integral
float2 erf(float2 x) {
float2 s = sign(x), a = abs(x);
x = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
x *= x;
return s - s / (x * x);
}
// Return the blurred mask along the x dimension
float roundedBoxShadowX(float x, float y, float sigma, float corner, float2 halfSize) {
float delta = min(halfSize.y - corner - abs(y), 0.0);
float curved = halfSize.x - corner + sqrt(max(0.0, corner * corner - delta * delta));
float2 integral = 0.5 + 0.5 * erf((x + float2(-curved, curved)) * (sqrt(0.5) / sigma));
return integral.y - integral.x;
}
// Return the mask for the shadow of a box from lower to upper
float roundedBoxShadow(float2 lower, float2 upper, float2 point, float sigma, float corner) {
// Center everything to make the math easier
float2 center = (lower + upper) * 0.5;
float2 halfSize = (upper - lower) * 0.5;
point -= center;
// The signal is only non-zero in a limited range, so don't waste samples
float low = point.y - halfSize.y;
float high = point.y + halfSize.y;
float start = clamp(-3.0 * sigma, low, high);
float end = clamp(3.0 * sigma, low, high);
// Accumulate samples (we can get away with surprisingly few samples)
float step = (end - start) / 4.0;
float y = start + step * 0.5;
float value = 0.0;
for (int i = 0; i < 4; i++) {
value += roundedBoxShadowX(point.x, point.y - y, sigma, corner, halfSize) * gaussian(y, sigma) * step;
y += step;
}
return value;
}
fragment float4 rect_fragment_shader(
RectFragmentData in [[stage_in]],
constant Uniforms *uniforms [[buffer(0)]])
{
float outer_distance;
float inner_distance;
// There are actually two different radii at play here - the inner
// (background) and outer (shape) radii. The inner radius is equal to the
// outer radius minus the border width, in order for the two curves to
// maintain a constant distance from each other.
float outer_corner_radius;
float inner_corner_radius;
// Length along the perimeter of (rounded) rectangle, starting from top left.
float length_along = 0.;
float2 pos_from_origin = in.position.xy - in.rect_origin;
float2 border_inner_corner = in.rect_corner;
if (in.position.y >= in.rect_center.y) {
// Bottom half
border_inner_corner.y -= in.border_bottom;
if (in.position.x >= in.rect_center.x) {
// Bottom right quadrant
border_inner_corner.x -= in.border_right;
outer_corner_radius = in.corner_radius_bottom_right;
inner_corner_radius = max(0.0, outer_corner_radius - in.border_bottom);
} else {
// Bottom left quadrant
border_inner_corner.x -= in.border_left;
outer_corner_radius = in.corner_radius_bottom_left;
inner_corner_radius = max(0.0, outer_corner_radius - in.border_bottom);
}
} else {
// Top half
border_inner_corner.y -= in.border_top;
if (in.position.x >= in.rect_center.x) {
// Top right quadrant
border_inner_corner.x -= in.border_right;
outer_corner_radius = in.corner_radius_top_right;
inner_corner_radius = max(0.0, outer_corner_radius - in.border_top);
} else {
// Top left quadrant
border_inner_corner.x -= in.border_left;
outer_corner_radius = in.corner_radius_top_left;
inner_corner_radius = max(0.0, outer_corner_radius - in.border_top);
}
}
float2 rect_bottom_right = in.rect_origin + in.rect_size;
outer_distance = distance_from_rect(in.position.xy, in.rect_center, in.rect_corner, outer_corner_radius);
inner_distance = distance_from_rect(in.position.xy, in.rect_center, border_inner_corner, inner_corner_radius);
float4 color;
if (in.drop_shadow_sigma > 0) {
color = in.drop_shadow_color;
// When we are rendering a drop shadow we need to pass in the positions
// of the original rect, so we figure them out from the padding.
// Note we subtract twice the padding, because the padding is specified
// in terms of padding on a single side.
float2 shadowed_rect_origin = in.rect_origin + in.drop_shadow_padding_factor;
float2 shadowed_rect_size = in.rect_size - 2 * in.drop_shadow_padding_factor;
color.a *= roundedBoxShadow(
shadowed_rect_origin,
shadowed_rect_origin + shadowed_rect_size,
in.pixel_position,
in.drop_shadow_sigma,
outer_corner_radius);
} else {
// Solid fill case (not a drop shadow)
float4 background_color = derive_color(in.position.xy, in.background_start, in.background_end, in.background_start_color, in.background_end_color);
float4 border_color = derive_color(in.position.xy, in.border_start, in.border_end, in.border_start_color, in.border_end_color);
// Adjust the opacity of the border color based on where the pixel lies
// between the background and the border.
border_color.a *= saturate(inner_distance + 0.5);
// Force the alpha value to 0 (fully transparent) if the pixel is
// outside the border.
//
// When we are outside the border, outer_distance is a larger positive
// value than inner_distance. When we are inside the border itself,
// outer_distance is negative and inner_distance is positive. When we
// are inside the inner border edge, outer_distance is more negative
// than inner_distance.
border_color.a *= inner_distance > outer_distance;
// Masks for pixels outside of inner rectangle or on border
bool is_horizontal_border = (in.position.y <= in.rect_origin.y + in.border_top) || (in.position.y >= rect_bottom_right.y - in.border_bottom);
bool is_vertical_border = (in.position.x <= in.rect_origin.x + in.border_left) || (in.position.x >= rect_bottom_right.x - in.border_right);
// Get length along the dash and gap segment and determine if pixel is in dash or gap
float length_on_dash_and_gap_segment_x = fmod(pos_from_origin.x, in.dash_length + in.gap_lengths.x);
float length_on_dash_and_gap_segment_y = fmod(pos_from_origin.y, in.dash_length + in.gap_lengths.y);
bool is_horizontal_dash = is_horizontal_border && (length_on_dash_and_gap_segment_x < in.dash_length);
bool is_vertical_dash = is_vertical_border && (length_on_dash_and_gap_segment_y < in.dash_length);
// Mask out any gaps in the border
border_color.a *= in.dash_length <= 0 || (is_horizontal_dash || is_vertical_dash);
// Perform proper alpha blending on the two colors, avoiding a
// divide-by-zero if both colors are fully transparent.
//
// See formula for "over" compositing here: https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
float alpha = border_color.a + background_color.a * (1.0 - border_color.a);
color.rgb = (border_color.rgb * border_color.a + background_color.rgb * background_color.a * (1.0 - border_color.a)) / (alpha + EPSILON);
color.a = alpha;
}
// If there's a corner radius we need to do some anti aliasing to smooth out the rounded corner effect.
if (outer_corner_radius > 0) {
color.a *= 1.0 - saturate(outer_distance + 0.5);
}
return color;
}
fragment float4 image_fragment_shader(
RectFragmentData in [[stage_in]],
texture2d<half> color_texture [[ texture(0) ]])
{
constexpr sampler texture_sampler (mag_filter::linear,
min_filter::linear);
// Sample the texture to obtain a color
const half4 color_sample = color_texture.sample(texture_sampler, in.texture_coordinate);
float4 color;
// If the image is an icon, use the provided icon_color instead of sampling from texture
if (in.is_icon) {
vector_float4 in_color = in.icon_color;
in_color.a *= color_sample.r;
color = float4(in_color);
} else {
color = float4(color_sample);
color.a *= in.icon_color.a;
}
float outer_corner_radius;
if (in.position.y >= in.rect_center.y) {
// Bottom half
if (in.position.x >= in.rect_center.x) {
// Bottom right quadrant
outer_corner_radius = in.corner_radius_bottom_right;
} else {
// Bottom left quadrant
outer_corner_radius = in.corner_radius_bottom_left;
}
} else {
// Top half
if (in.position.x >= in.rect_center.x) {
// Top right quadrant
outer_corner_radius = in.corner_radius_top_right;
} else {
// Top left quadrant
outer_corner_radius = in.corner_radius_top_left;
}
}
float outer_distance = distance_from_rect(in.position.xy, in.rect_center, in.rect_corner, outer_corner_radius);
// If there's a corner radius we need to do some anti aliasing to smooth out the rounded corner effect.
if (outer_corner_radius > 0) {
color.a *= 1.0 - saturate(outer_distance + 0.5);
}
return color;
}
vertex GlyphFragmentData
glyph_vertex_shader(
uint vertex_id [[vertex_id]],
uint instance_id [[instance_id]],
constant vector_float2 *vertices [[buffer(0)]],
const device PerGlyphUniforms *glyph_uniforms [[buffer(1)]],
constant Uniforms *uniforms [[buffer(2)]])
{
const device PerGlyphUniforms *glyph = &glyph_uniforms[instance_id];
float2 pixel_pos = vertices[vertex_id] * glyph->size + glyph->origin;
// Use floor here to vertically align the glyph to the pixel grid.
// If it's not aligned to the grid, the fragment shader will do its
// own interpolation, which makes it so we don't use the anti-aliasing
// from core text, which is what we want. We don't force the glyph to a
// horizontal pixel position because we rasterize the glyph at multiple
// subpixel positions, and so the very slight linear interpolation here
// won't produce a fuzzy glyph, just a correctly-positioned one.
pixel_pos = float2(pixel_pos.x, floor(pixel_pos.y));
// Evaluating the glyphs fade effect. Note that the fade may go in two different directions:
// - Right to left (default) - where the opaque side is on the right, and transparent on the left
// (in this case, the start_fade < end_fade; start is where the fade is transparent)
// - Left to right - where the opaque side is on the left, and it fades towards the right side.
// In this case, start_fade > end_fade, and the opaque side is on the left (end_fade).
// To clarify: fade_start is ALWAYS where the fade is transparent, and fade_end is ALWAYS where
// the opaque part is, this is reflected in how we compute width, dist, and alpha.
float fade_width = fabs(glyph->fade_end - glyph->fade_start);
float fade_dist = pixel_pos.x - fmin(glyph->fade_start, glyph->fade_end);
float fade_alpha;
if (glyph->fade_end < glyph->fade_start) { // left-to-right case
fade_alpha = fade_dist / fade_width;
} else { // right-to-left case
fade_alpha = 1 - fade_dist / fade_width;
}
vector_float2 device_pos = pixel_pos / uniforms->viewport_size * vector_float2(2.0, -2.0) + vector_float2(-1.0, 1.0);
vector_float2 texture_coordinate = vector_float2(glyph->uv_left, glyph->uv_top) + vertices[vertex_id] * vector_float2(glyph->uv_width, glyph->uv_height);
GlyphFragmentData out;
out.position = vector_float4(device_pos, 0.0, 1.0);
out.rect_corner = glyph->size / 2.0;
out.rect_center = glyph->origin + out.rect_corner;
out.texture_coordinate = texture_coordinate;
out.fade_alpha = fade_alpha;
out.color = glyph->color;
out.is_emoji = glyph->is_emoji;
return out;
}
fragment float4 glyph_fragment_shader(
GlyphFragmentData in [[stage_in]],
texture2d<half> color_texture [[ texture(0) ]]
) {
// Sample the texture to obtain a color.
constexpr sampler texture_sampler (mag_filter::linear, min_filter::linear);
const float4 color_sample = float4(color_texture.sample(texture_sampler, in.texture_coordinate));
// Use the input color for non-emoji, and the sampled color for emoji.
float4 color = mix(in.color, color_sample, float(in.is_emoji));
// Multiply alpha by the sampled color's red channel for non-emoji.
color.a *= max(color_sample.r, float(in.is_emoji));
// Apply the fade.
color.a *= saturate(in.fade_alpha);
return color;
}
@@ -0,0 +1,28 @@
mod metal;
mod renderer;
mod renderer_manager;
#[cfg(wgpu)]
mod wgpu;
pub use self::metal::is_integrated_gpu;
pub use renderer::{Device, Renderer};
pub use renderer_manager::RendererManager;
/// Returns `true` if a low power GPU is available for rendering. Typically, this is true for
/// machines with two GPUs -- a dedicated discrete high-performance GPU and a lower power
/// integrated GPU.
pub fn is_low_power_gpu_available() -> bool {
cfg_if::cfg_if! {
if #[cfg(wgpu)] {
crate::r#async::block_on(crate::rendering::wgpu::is_low_power_gpu_available())
} else {
let devices = ::metal::Device::all();
let gpu_count = devices.len();
gpu_count > 1
&& devices
.iter()
.any(metal::is_integrated_gpu)
}
}
}
@@ -0,0 +1,63 @@
use crate::platform::mac::rendering::is_integrated_gpu;
use crate::platform::mac::window::WindowState;
use cocoa::base::id;
use galaxyui_core::rendering::{
GPUBackend, GPUDeviceInfo, GPUDeviceType, GPUPowerPreference, OnGPUDeviceSelected,
};
use galaxyui_core::{fonts, Scene};
/// Trait to render the [`Scene`] onto the screen using the provided [`WindowState`].
pub trait Renderer {
fn render(&mut self, scene: &Scene, window: &WindowState, font_cache: &fonts::Cache);
fn resize(&mut self, window: &WindowState);
}
/// Set of available physical graphics devices that can be used to render.
#[allow(clippy::upper_case_acronyms)]
pub enum Device {
#[allow(dead_code)]
Metal(metal::Device),
#[cfg(wgpu)]
WGPU(Box<crate::rendering::wgpu::Resources>),
}
impl Device {
pub fn new(
_metal_device: metal::Device,
_native_view: id,
_native_window: id,
_gpu_power_preference: GPUPowerPreference,
on_gpu_device_info: Box<OnGPUDeviceSelected>,
) -> Self {
#[cfg(not(wgpu))]
{
let gpu_device_info = get_gpu_device_info(&_metal_device);
on_gpu_device_info(gpu_device_info);
Device::Metal(_metal_device)
}
#[cfg(wgpu)]
{
Device::new_wgpu(_native_view, _gpu_power_preference, on_gpu_device_info)
.expect("unable to create wgpu device")
}
}
}
#[cfg_attr(wgpu, allow(dead_code))]
fn get_gpu_device_info(device: &metal::Device) -> GPUDeviceInfo {
let device_type = if is_integrated_gpu(device) {
GPUDeviceType::IntegratedGpu
} else {
GPUDeviceType::DiscreteGpu
};
GPUDeviceInfo {
device_type,
device_name: device.name().into(),
// Mimic wgpu by setting the driver name and info to empty strings when
// rendering on Metal. See https://github.com/gfx-rs/wgpu/blob/8129897ccbff869ef48a3b53a4cdd8a8a21840f9/wgpu-hal/src/metal/mod.rs#L135.
driver_name: String::new(),
driver_info: String::new(),
backend: GPUBackend::Metal,
}
}
@@ -0,0 +1,44 @@
use pathfinder_geometry::vector::Vector2F;
use super::{
metal,
renderer::{Device, Renderer},
};
pub struct RendererManager {
metal_renderer_manager: metal::RendererManager,
#[cfg(wgpu)]
wgpu_renderer_manager: super::wgpu::RendererManager,
}
impl Default for RendererManager {
fn default() -> Self {
Self::new()
}
}
impl RendererManager {
pub fn new() -> Self {
Self {
metal_renderer_manager: metal::RendererManager::new(),
#[cfg(wgpu)]
wgpu_renderer_manager: super::wgpu::RendererManager::new(),
}
}
/// Returns a [`Renderer`] that can be used to render on the given [`Device`].
#[allow(unused_variables)]
pub fn renderer_for_device(
&mut self,
device: &Device,
window_size: Vector2F,
) -> &mut dyn Renderer {
match device {
Device::Metal(device) => self.metal_renderer_manager.renderer_for_device(device),
#[cfg(wgpu)]
Device::WGPU(resources) => self
.wgpu_renderer_manager
.renderer_for_resources(resources, window_size),
}
}
}
@@ -0,0 +1,93 @@
mod renderer;
mod renderer_manager;
use crate::rendering::wgpu::Resources;
use crate::{platform::mac::rendering::Device, rendering::GPUPowerPreference};
use anyhow::{anyhow, Result};
pub use renderer_manager::RendererManager;
use crate::rendering::OnGPUDeviceSelected;
use cocoa::{appkit::NSView, base::id};
use pathfinder_geometry::vector::vec2f;
use std::ptr::NonNull;
use wgpu::rwh::{
AppKitDisplayHandle, AppKitWindowHandle, DisplayHandle, HandleError, HasDisplayHandle,
HasWindowHandle, RawDisplayHandle, RawWindowHandle, WindowHandle,
};
impl Device {
/// Constructs a new [`Device`] to render using WGPU.
pub fn new_wgpu(
native_view: id,
gpu_power_preference: GPUPowerPreference,
on_gpu_device_info: Box<OnGPUDeviceSelected>,
) -> Result<Device> {
let view_frame = unsafe { NSView::frame(native_view) };
let surface_size = vec2f(view_frame.size.width as f32, view_frame.size.height as f32);
let appkit_window_handle = AppKitWindowHandle::new(
NonNull::new(native_view)
.ok_or_else(|| anyhow!("Received null NSView pointer"))?
.cast(),
);
let window_handle =
unsafe { WindowHandle::borrow_raw(RawWindowHandle::AppKit(appkit_window_handle)) };
let display_handle = unsafe {
DisplayHandle::borrow_raw(RawDisplayHandle::AppKit(AppKitDisplayHandle::new()))
};
let trusted_window = TrustedWindow {
window_handle,
display_handle,
};
crate::rendering::wgpu::init_wgpu_instance(Box::new(trusted_window));
let resources = Resources::new(
trusted_window,
gpu_power_preference,
None,
&on_gpu_device_info,
surface_size,
false, /* downrank_non_nvidia_vulkan_adapters */
)?;
Ok(Device::WGPU(Box::new(resources)))
}
}
/// Wrapper struct that implements the [`HasRawWindowHandle`] and [`HasRawDisplayHandle`] traits.
/// The raw-window-handle crate purposefully does not provide a blanket implementation of this trait
/// for any implementation of [`RawWindowHandle`] or [`RawDisplayHandle`] because it's not
/// guaranteed that the underlying window won't become invalid while the `WindowHandle` is alive.
/// In the case of Warp this _should_ be safe because we ultimately deallocate the native window
/// when [`crate::platform::mac::Window`] is deallocated (once a `Window` is deallocated, there
/// are no pointers to the native window anymore, which cause it to to be deallocated via the
/// `warp_dealloc_window` callback).
/// See <https://github.com/rust-windowing/raw-window-handle/pull/73> for more information on the
/// safety requirements of implementing the [`HasRawWindowHandle`] trait.
#[derive(Copy, Clone, Debug)]
struct TrustedWindow {
window_handle: WindowHandle<'static>,
display_handle: DisplayHandle<'static>,
}
// THIS IS INCREDIBLY UNSAFE!!! DO NOT DO THIS!!!
//
// That said, we're not using this codepath in production, and it unblocks us
// moving to wgpu 0.19 (an important migration for the Linux target), so we're
// doing this and covering our eyes for now, with the intention of fixing it or
// removing support for `wpgu` in our macOS backend.
unsafe impl Send for TrustedWindow {}
unsafe impl Sync for TrustedWindow {}
impl HasWindowHandle for TrustedWindow {
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
Ok(self.window_handle)
}
}
impl HasDisplayHandle for TrustedWindow {
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
Ok(self.display_handle)
}
}
@@ -0,0 +1,46 @@
use crate::platform::mac::rendering::Device;
use crate::platform::mac::window::WindowState;
use crate::rendering::wgpu::{Renderer, Resources};
use crate::{fonts, Scene};
impl super::super::Renderer for Renderer {
fn render(&mut self, scene: &Scene, window: &WindowState, font_cache: &fonts::Cache) {
let _ = Renderer::render(
self,
scene,
window.unwrap_wgpu_resources(),
&|glyph_key, scale, subpixel_alignment, glyph_config, format| {
font_cache.rasterized_glyph(
glyph_key,
scale,
subpixel_alignment,
glyph_config,
format,
)
},
&|glyph_key, scale, alignment| {
font_cache.glyph_raster_bounds(glyph_key, scale, alignment)
},
window.physical_size(),
None,
window.capture_callback.borrow_mut().take(),
);
}
fn resize(&mut self, window: &WindowState) {
let _ = window
.unwrap_wgpu_resources()
.update_surface_size(window.physical_size());
}
}
impl WindowState {
fn unwrap_wgpu_resources(&self) -> &Resources {
match self.device().unwrap() {
Device::Metal(_) => {
panic!("called the WGPU renderer with a metal device");
}
Device::WGPU(resources) => resources,
}
}
}
@@ -0,0 +1,42 @@
use crate::rendering::wgpu::{Renderer, Resources};
use crate::rendering::GlyphConfig;
use pathfinder_geometry::vector::Vector2F;
use std::collections::HashMap;
use std::hash::{DefaultHasher, Hash, Hasher};
use wgpu::Device;
pub struct RendererManager {
renderers: HashMap<DeviceID, Renderer>,
}
#[derive(Copy, Clone, Hash, Eq, PartialEq)]
struct DeviceID(u64);
impl From<&Device> for DeviceID {
fn from(value: &Device) -> Self {
let mut s = DefaultHasher::new();
value.hash(&mut s);
DeviceID(s.finish())
}
}
impl RendererManager {
pub fn new() -> Self {
Self {
renderers: Default::default(),
}
}
/// Returns a [`Renderer`] identified by the device contained in [`Resources`].
pub fn renderer_for_resources(
&mut self,
resources: &Resources,
_window_size: Vector2F,
) -> &mut Renderer {
use std::collections::hash_map::Entry::*;
match self.renderers.entry((&resources.device).into()) {
Occupied(entry) => entry.into_mut(),
Vacant(entry) => entry.insert(Renderer::new(resources, GlyphConfig::default())),
}
}
}