use cgmath::Matrix4; use glow::{Context as GLContext, HasContext}; #[cfg(not(target_arch = "wasm32"))] use itertools::Itertools; use lyon::tessellation::geometry_builder::{BuffersBuilder, VertexBuffers}; use lyon::tessellation::{ FillAttributes, FillOptions, FillTessellator, StrokeAttributes, StrokeOptions, StrokeTessellator, }; use sixtyfps_corelib::abi::datastructures::{ Color, ComponentWindow, ComponentWindowOpaque, Point, Rect, RenderingPrimitive, Resource, Size, }; use sixtyfps_corelib::graphics::{ FillStyle, Frame as GraphicsFrame, GraphicsBackend, GraphicsWindow, HasRenderingPrimitive, RenderingPrimitivesBuilder, }; use smallvec::{smallvec, SmallVec}; use std::cell::RefCell; extern crate alloc; use alloc::rc::Rc; mod texture; use texture::{GLTexture, TextureAtlas}; mod shader; use shader::{ImageShader, PathShader}; #[cfg(not(target_arch = "wasm32"))] use shader::GlyphShader; mod buffers; use buffers::{GLArrayBuffer, GLIndexBuffer}; #[cfg(not(target_arch = "wasm32"))] mod text; #[cfg(not(target_arch = "wasm32"))] mod fontcache; #[cfg(not(target_arch = "wasm32"))] use fontcache::FontCache; #[cfg(not(target_arch = "wasm32"))] struct PlatformData { font_cache: FontCache, glyph_shader: GlyphShader, } #[cfg(target_arch = "wasm32")] struct PlatformData {} impl PlatformData { #[cfg(not(target_arch = "wasm32"))] fn new(context: &glow::Context) -> Self { Self { font_cache: FontCache::default(), glyph_shader: GlyphShader::new(&context) } } #[cfg(target_arch = "wasm32")] fn new(_context: &glow::Context) -> Self { Self {} } #[cfg(target_arch = "wasm32")] fn drop(&mut self, _context: &glow::Context) {} #[cfg(not(target_arch = "wasm32"))] fn drop(&mut self, context: &glow::Context) { self.glyph_shader.drop(&context); } } #[derive(Copy, Clone)] pub(crate) struct Vertex { _pos: [f32; 2], } #[cfg(not(target_arch = "wasm32"))] struct GlyphRun { vertices: GLArrayBuffer, texture_vertices: GLArrayBuffer, texture: GLTexture, vertex_count: i32, } enum GLRenderingPrimitive { FillPath { vertices: GLArrayBuffer, indices: GLIndexBuffer, style: FillStyle, }, Texture { vertices: GLArrayBuffer, texture_vertices: GLArrayBuffer, texture: GLTexture, }, #[cfg(not(target_arch = "wasm32"))] GlyphRuns { glyph_runs: Vec, color: Color, }, } pub struct GLRenderer { context: Rc, path_shader: PathShader, image_shader: ImageShader, platform_data: Rc>, texture_atlas: Rc>, #[cfg(target_arch = "wasm32")] window: winit::window::Window, #[cfg(not(target_arch = "wasm32"))] windowed_context: Option>, } pub struct GLRenderingPrimitivesBuilder { context: Rc, fill_tesselator: FillTessellator, stroke_tesselator: StrokeTessellator, texture_atlas: Rc>, #[cfg(not(target_arch = "wasm32"))] platform_data: Rc>, #[cfg(not(target_arch = "wasm32"))] windowed_context: glutin::WindowedContext, } pub struct GLFrame { context: Rc, path_shader: PathShader, image_shader: ImageShader, #[cfg(not(target_arch = "wasm32"))] platform_data: Rc>, root_matrix: cgmath::Matrix4, #[cfg(not(target_arch = "wasm32"))] windowed_context: glutin::WindowedContext, } impl GLRenderer { pub fn new( event_loop: &winit::event_loop::EventLoop<()>, window_builder: winit::window::WindowBuilder, ) -> GLRenderer { #[cfg(not(target_arch = "wasm32"))] let (windowed_context, context) = { let windowed_context = glutin::ContextBuilder::new() .with_vsync(true) .build_windowed(window_builder, &event_loop) .unwrap(); let windowed_context = unsafe { windowed_context.make_current().unwrap() }; let gl_context = glow::Context::from_loader_function(|s| { windowed_context.get_proc_address(s) as *const _ }); (windowed_context, gl_context) }; #[cfg(target_arch = "wasm32")] let (window, context) = { let canvas = web_sys::window() .unwrap() .document() .unwrap() .get_element_by_id("canvas") .unwrap() .dyn_into::() .unwrap(); use winit::platform::web::WindowBuilderExtWebSys; use winit::platform::web::WindowExtWebSys; let window = window_builder.with_canvas(Some(canvas)).build(&event_loop).unwrap(); use wasm_bindgen::JsCast; let webgl1_context = window .canvas() .get_context("webgl") .unwrap() .unwrap() .dyn_into::() .unwrap(); (window, glow::Context::from_webgl1_context(webgl1_context)) }; let vertex_array_object = unsafe { context.create_vertex_array().expect("Cannot create vertex array") }; unsafe { context.bind_vertex_array(Some(vertex_array_object)); } let path_shader = PathShader::new(&context); let image_shader = ImageShader::new(&context); let platform_data = Rc::new(RefCell::new(PlatformData::new(&context))); GLRenderer { context: Rc::new(context), path_shader, image_shader, platform_data, texture_atlas: Rc::new(RefCell::new(TextureAtlas::new())), #[cfg(target_arch = "wasm32")] window, #[cfg(not(target_arch = "wasm32"))] windowed_context: Some(unsafe { windowed_context.make_not_current().unwrap() }), } } } type GLRenderingPrimitives = SmallVec<[GLRenderingPrimitive; 1]>; pub struct OpaqueRenderingPrimitive { gl_primitives: GLRenderingPrimitives, rendering_primitive: RenderingPrimitive, } impl HasRenderingPrimitive for OpaqueRenderingPrimitive { fn primitive(&self) -> &RenderingPrimitive { &self.rendering_primitive } } impl GraphicsBackend for GLRenderer { type LowLevelRenderingPrimitive = OpaqueRenderingPrimitive; type Frame = GLFrame; type RenderingPrimitivesBuilder = GLRenderingPrimitivesBuilder; fn new_rendering_primitives_builder(&mut self) -> Self::RenderingPrimitivesBuilder { #[cfg(not(target_arch = "wasm32"))] let current_windowed_context = unsafe { self.windowed_context.take().unwrap().make_current().unwrap() }; GLRenderingPrimitivesBuilder { context: self.context.clone(), fill_tesselator: FillTessellator::new(), stroke_tesselator: StrokeTessellator::new(), texture_atlas: self.texture_atlas.clone(), #[cfg(not(target_arch = "wasm32"))] platform_data: self.platform_data.clone(), #[cfg(not(target_arch = "wasm32"))] windowed_context: current_windowed_context, } } fn finish_primitives(&mut self, _builder: Self::RenderingPrimitivesBuilder) { #[cfg(not(target_arch = "wasm32"))] { self.windowed_context = Some(unsafe { _builder.windowed_context.make_not_current().unwrap() }); } } fn new_frame(&mut self, width: u32, height: u32, clear_color: &Color) -> GLFrame { #[cfg(not(target_arch = "wasm32"))] let current_windowed_context = unsafe { self.windowed_context.take().unwrap().make_current().unwrap() }; unsafe { self.context.viewport(0, 0, width as i32, height as i32); self.context.enable(glow::BLEND); self.context.blend_func(glow::ONE, glow::ONE_MINUS_SRC_ALPHA); } let (r, g, b, a) = clear_color.as_rgba_f32(); unsafe { self.context.clear_color(r, g, b, a); self.context.clear(glow::COLOR_BUFFER_BIT); }; GLFrame { context: self.context.clone(), path_shader: self.path_shader.clone(), image_shader: self.image_shader.clone(), #[cfg(not(target_arch = "wasm32"))] platform_data: self.platform_data.clone(), root_matrix: cgmath::ortho(0.0, width as f32, height as f32, 0.0, -1., 1.0), #[cfg(not(target_arch = "wasm32"))] windowed_context: current_windowed_context, } } fn present_frame(&mut self, _frame: Self::Frame) { #[cfg(not(target_arch = "wasm32"))] { _frame.windowed_context.swap_buffers().unwrap(); self.windowed_context = Some(unsafe { _frame.windowed_context.make_not_current().unwrap() }); } } fn window(&self) -> &winit::window::Window { #[cfg(not(target_arch = "wasm32"))] return self.windowed_context.as_ref().unwrap().window(); #[cfg(target_arch = "wasm32")] return &self.window; } } impl RenderingPrimitivesBuilder for GLRenderingPrimitivesBuilder { type LowLevelRenderingPrimitive = OpaqueRenderingPrimitive; fn create(&mut self, primitive: RenderingPrimitive) -> Self::LowLevelRenderingPrimitive { OpaqueRenderingPrimitive { gl_primitives: match &primitive { RenderingPrimitive::NoContents => smallvec::SmallVec::new(), RenderingPrimitive::Rectangle { x: _, y: _, width, height, color } => { use lyon::math::Point; let mut rect_path = lyon::path::Path::builder(); rect_path.move_to(Point::new(0., 0.0)); rect_path.line_to(Point::new(*width, 0.0)); rect_path.line_to(Point::new(*width, *height)); rect_path.line_to(Point::new(0.0, *height)); rect_path.close(); self.fill_path(&rect_path.build(), FillStyle::SolidColor(*color)) .into_iter() .collect() } RenderingPrimitive::Image { x: _, y: _, source } => { match source { Resource::AbsoluteFilePath(path) => { let mut image_path = std::env::current_exe().unwrap(); image_path.pop(); // pop of executable name image_path.push(&*path.clone()); let image = image::open(image_path.as_path()).unwrap().into_rgba(); smallvec![self.create_image(image)] } Resource::EmbeddedData(slice) => { let image_slice = slice.as_slice(); let image = image::load_from_memory(image_slice).unwrap().to_rgba(); smallvec![self.create_image(image)] } Resource::None => SmallVec::new(), } } RenderingPrimitive::Text { x: _, y: _, text, font_family, font_pixel_size, color, } => { let pixel_size = if *font_pixel_size != 0. { *font_pixel_size } else { 48.0 * 72. / 96. }; smallvec![self.create_glyph_runs(text, font_family, pixel_size, *color)] } RenderingPrimitive::Path { x: _, y: _, width, height, elements, fill_color, stroke_color, stroke_width, } => { let mut primitives = SmallVec::new(); let path_iter = elements.iter().fitted(*width, *height); if *fill_color != Color::TRANSPARENT { primitives.extend( self.fill_path(path_iter.iter(), FillStyle::SolidColor(*fill_color)) .into_iter(), ); } if *stroke_color != Color::TRANSPARENT { primitives.extend( self.stroke_path(path_iter.iter(), *stroke_color, *stroke_width) .into_iter(), ); } primitives } }, rendering_primitive: primitive, } } } impl GLRenderingPrimitivesBuilder { fn fill_path( &mut self, path: impl IntoIterator, style: FillStyle, ) -> Option { let mut geometry: VertexBuffers = VertexBuffers::new(); let fill_opts = FillOptions::default(); self.fill_tesselator .tessellate( path, &fill_opts, &mut BuffersBuilder::new( &mut geometry, |pos: lyon::math::Point, _: FillAttributes| Vertex { _pos: [pos.x as f32, pos.y as f32], }, ), ) .unwrap(); if geometry.vertices.len() == 0 || geometry.indices.len() == 0 { return None; } let vertices = GLArrayBuffer::new(&self.context, &geometry.vertices); let indices = GLIndexBuffer::new(&self.context, &geometry.indices); Some(GLRenderingPrimitive::FillPath { vertices, indices, style }.into()) } fn stroke_path( &mut self, path: impl IntoIterator, stroke_color: Color, stroke_width: f32, ) -> Option { let mut geometry: VertexBuffers = VertexBuffers::new(); let stroke_opts = StrokeOptions::DEFAULT.with_line_width(stroke_width); self.stroke_tesselator .tessellate( path, &stroke_opts, &mut BuffersBuilder::new( &mut geometry, |pos: lyon::math::Point, _: StrokeAttributes| Vertex { _pos: [pos.x as f32, pos.y as f32], }, ), ) .unwrap(); if geometry.vertices.len() == 0 || geometry.indices.len() == 0 { return None; } let vertices = GLArrayBuffer::new(&self.context, &geometry.vertices); let indices = GLIndexBuffer::new(&self.context, &geometry.indices); Some( GLRenderingPrimitive::FillPath { vertices, indices, style: FillStyle::SolidColor(stroke_color), } .into(), ) } fn create_image( &mut self, image: image::ImageBuffer, Vec>, ) -> GLRenderingPrimitive { let source_size = image.dimensions(); let rect = Rect::new(Point::new(0.0, 0.0), Size::new(source_size.0 as f32, source_size.1 as f32)); let vertex1 = Vertex { _pos: [rect.min_x(), rect.min_y()] }; let vertex2 = Vertex { _pos: [rect.max_x(), rect.min_y()] }; let vertex3 = Vertex { _pos: [rect.max_x(), rect.max_y()] }; let vertex4 = Vertex { _pos: [rect.min_x(), rect.max_y()] }; let mut atlas = self.texture_atlas.borrow_mut(); let atlas_allocation = atlas.allocate_image_in_atlas(&self.context, image); let vertices = GLArrayBuffer::new( &self.context, &vec![vertex1, vertex2, vertex3, vertex1, vertex3, vertex4], ); let texture_vertices = GLArrayBuffer::new(&self.context, &atlas_allocation.sub_texture.normalized_coordinates); GLRenderingPrimitive::Texture { vertices, texture_vertices, texture: atlas_allocation.sub_texture.texture, } } #[cfg(not(target_arch = "wasm32"))] fn create_glyph_runs( &mut self, text: &str, font_family: &str, pixel_size: f32, color: Color, ) -> GLRenderingPrimitive { let mut pd = self.platform_data.borrow_mut(); let font_cache = &mut pd.font_cache; let font = font_cache.find_font(font_family, pixel_size); let mut font = font.borrow_mut(); let glyphs = font.string_to_glyphs(&self.context, &mut self.texture_atlas.borrow_mut(), text); let mut x = 0.; let glyph_runs = font .layout_glyphs(glyphs) .map(|cached_glyph| { let glyph_width = cached_glyph.glyph_allocation.sub_texture.texture_coordinates.width() as f32; let glyph_height = cached_glyph.glyph_allocation.sub_texture.texture_coordinates.height() as f32; let vertex1 = Vertex { _pos: [x, 0.] }; let vertex2 = Vertex { _pos: [x + glyph_width, 0.] }; let vertex3 = Vertex { _pos: [x + glyph_width, glyph_height] }; let vertex4 = Vertex { _pos: [x, glyph_height] }; let vertices = [vertex1, vertex2, vertex3, vertex1, vertex3, vertex4]; let texture_vertices = cached_glyph.glyph_allocation.sub_texture.normalized_coordinates; let texture = cached_glyph.glyph_allocation.sub_texture.texture; x += cached_glyph.advance; (vertices, texture_vertices, texture) }) .group_by(|(_, _, texture)| *texture) .into_iter() .map(|(texture, glyph_it)| { let glyph_count = glyph_it.size_hint().0; let mut vertices: Vec = Vec::with_capacity(glyph_count * 6); let mut texture_vertices: Vec = Vec::with_capacity(glyph_count * 6); for (glyph_vertices, glyph_texture_vertices) in glyph_it.map(|(vertices, texture_vertices, _)| (vertices, texture_vertices)) { vertices.extend(&glyph_vertices); texture_vertices.extend(&glyph_texture_vertices); } let vertex_count = vertices.len() as i32; GlyphRun { vertices: GLArrayBuffer::new(&self.context, &vertices), texture_vertices: GLArrayBuffer::new(&self.context, &texture_vertices), texture, vertex_count, } }) .collect(); GLRenderingPrimitive::GlyphRuns { glyph_runs, color } } #[cfg(target_arch = "wasm32")] fn create_glyph_runs( &mut self, text: &str, font_family: &str, pixel_size: f32, color: Color, ) -> GLRenderingPrimitive { let text_canvas = web_sys::window() .unwrap() .document() .unwrap() .create_element("canvas") .unwrap() .dyn_into::() .unwrap(); use wasm_bindgen::JsCast; let canvas_context = text_canvas .get_context("2d") .unwrap() .unwrap() .dyn_into::() .unwrap(); canvas_context.set_font(&format!("{}px \"{}\"", pixel_size, font_family)); let text_metrics = canvas_context.measure_text(text).unwrap(); text_canvas.set_width(text_metrics.width() as _); text_canvas.set_height(pixel_size as _); text_canvas.style().set_property("width", &format!("{}px", text_metrics.width())).unwrap(); text_canvas.style().set_property("height", &format!("{}px", pixel_size)).unwrap(); // Re-apply after resize :( canvas_context.set_font(&format!("{}px \"{}\"", pixel_size, font_family)); canvas_context.set_text_align("center"); canvas_context.set_text_baseline("middle"); canvas_context.set_fill_style(&wasm_bindgen::JsValue::from_str("transparent")); canvas_context.fill_rect(0., 0., text_canvas.width() as _, text_canvas.height() as _); let (r, g, b, a) = color.as_rgba_u8(); canvas_context.set_fill_style(&wasm_bindgen::JsValue::from_str(&format!( "rgba({}, {}, {}, {})", r, g, b, a ))); canvas_context .fill_text(text, (text_canvas.width() / 2) as _, (text_canvas.height() / 2) as _) .unwrap(); let texture = GLTexture::new_from_canvas(&self.context, &text_canvas); let rect = Rect::new( Point::new(0.0, 0.0), Size::new(text_canvas.width() as f32, text_canvas.height() as f32), ); let vertex1 = Vertex { _pos: [rect.min_x(), rect.min_y()] }; let vertex2 = Vertex { _pos: [rect.max_x(), rect.min_y()] }; let vertex3 = Vertex { _pos: [rect.max_x(), rect.max_y()] }; let vertex4 = Vertex { _pos: [rect.min_x(), rect.max_y()] }; let tex_vertex1 = Vertex { _pos: [0., 0.] }; let tex_vertex2 = Vertex { _pos: [1., 0.] }; let tex_vertex3 = Vertex { _pos: [1., 1.] }; let tex_vertex4 = Vertex { _pos: [0., 1.] }; let normalized_coordinates: [Vertex; 6] = [tex_vertex1, tex_vertex2, tex_vertex3, tex_vertex1, tex_vertex3, tex_vertex4]; let vertices = GLArrayBuffer::new( &self.context, &vec![vertex1, vertex2, vertex3, vertex1, vertex3, vertex4], ); let texture_vertices = GLArrayBuffer::new(&self.context, &normalized_coordinates); GLRenderingPrimitive::Texture { vertices, texture_vertices, texture } } } impl GraphicsFrame for GLFrame { type LowLevelRenderingPrimitive = OpaqueRenderingPrimitive; fn render_primitive(&mut self, primitive: &OpaqueRenderingPrimitive, transform: &Matrix4) { let matrix = self.root_matrix * transform; let gl_matrix: [f32; 16] = [ matrix.x[0], matrix.x[1], matrix.x[2], matrix.x[3], matrix.y[0], matrix.y[1], matrix.y[2], matrix.y[3], matrix.z[0], matrix.z[1], matrix.z[2], matrix.z[3], matrix.w[0], matrix.w[1], matrix.w[2], matrix.w[3], ]; primitive.gl_primitives.iter().for_each(|gl_primitive| match gl_primitive { GLRenderingPrimitive::FillPath { vertices, indices, style } => { let (r, g, b, a) = match style { FillStyle::SolidColor(color) => color.as_rgba_f32(), }; self.path_shader.bind(&self.context, &gl_matrix, &[r, g, b, a], vertices, indices); unsafe { self.context.draw_elements( glow::TRIANGLES, indices.len, glow::UNSIGNED_SHORT, 0, ); } } GLRenderingPrimitive::Texture { vertices, texture_vertices, texture } => { self.image_shader.bind( &self.context, &gl_matrix, texture, vertices, texture_vertices, ); unsafe { self.context.draw_arrays(glow::TRIANGLES, 0, 6); } } #[cfg(not(target_arch = "wasm32"))] GLRenderingPrimitive::GlyphRuns { glyph_runs, color } => { let (r, g, b, a) = color.as_rgba_f32(); for GlyphRun { vertices, texture_vertices, texture, vertex_count } in glyph_runs { self.platform_data.borrow().glyph_shader.bind( &self.context, &gl_matrix, &[r, g, b, a], texture, vertices, texture_vertices, ); unsafe { self.context.draw_arrays(glow::TRIANGLES, 0, *vertex_count); } } } }); } } impl Drop for GLRenderer { fn drop(&mut self) { self.path_shader.drop(&self.context); self.image_shader.drop(&self.context); self.platform_data.borrow_mut().drop(&self.context); } } #[no_mangle] pub unsafe extern "C" fn sixtyfps_component_window_gl_renderer_init( out: *mut ComponentWindowOpaque, ) { assert_eq!( core::mem::size_of::(), core::mem::size_of::() ); core::ptr::write(out as *mut ComponentWindow, create_gl_window()); } pub fn create_gl_window() -> ComponentWindow { ComponentWindow::new(GraphicsWindow::new(|event_loop, window_builder| { GLRenderer::new(&event_loop.get_winit_event_loop(), window_builder) })) }