Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though
This commit is contained in:
@@ -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())),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user