feat(terminal): recover webgl renderer context
This commit is contained in:
@@ -27,8 +27,10 @@ use super::mouse::{
|
||||
};
|
||||
use super::protocol::{ClientTerminalMessage, ServerTerminalMessage};
|
||||
use super::renderer::{
|
||||
CanvasDomState, CanvasPlanInput, CanvasSurfaceFrame, RenderCanvasSurfaceRequest,
|
||||
WebGlRendererState, canvas_render_plan, render_canvas_surface, render_webgl,
|
||||
CanvasDomState, CanvasPlanInput, CanvasSurfaceFrame, CanvasSurfaceMode,
|
||||
RenderCanvasSurfaceRequest, WebGlContextLifecycle, WebGlRendererState,
|
||||
attach_webgl_context_lifecycle, canvas_render_plan, render_canvas_surface, render_webgl,
|
||||
set_canvas_surface_mode, webgl_surface_mode,
|
||||
};
|
||||
use super::scrollback::ViewMode;
|
||||
use super::selection::{Selection, SelectionRect, selection_rects};
|
||||
@@ -472,6 +474,7 @@ pub fn TerminalPanel(
|
||||
let terminal_ref = NodeRef::<html::Div>::new();
|
||||
let ime_ref = NodeRef::<html::Textarea>::new();
|
||||
let canvas_ref = NodeRef::<html::Canvas>::new();
|
||||
let canvas_fallback_ref = NodeRef::<html::Canvas>::new();
|
||||
let measure_ref = NodeRef::<html::Span>::new();
|
||||
let connection_status = RwSignal::new("Connecting".to_owned());
|
||||
let ws_signal = RwSignal::new(None::<WebSocket>);
|
||||
@@ -525,8 +528,11 @@ pub fn TerminalPanel(
|
||||
Rc::new(std::cell::Cell::new(None));
|
||||
let canvas_state: Rc<std::cell::RefCell<Option<CanvasDomState>>> =
|
||||
Rc::new(std::cell::RefCell::new(None));
|
||||
let canvas_fallback_state: Rc<std::cell::RefCell<Option<CanvasDomState>>> =
|
||||
Rc::new(std::cell::RefCell::new(None));
|
||||
let webgl_state: Rc<std::cell::RefCell<Option<WebGlRendererState>>> =
|
||||
Rc::new(std::cell::RefCell::new(None));
|
||||
let webgl_context_lost: Rc<std::cell::Cell<bool>> = Rc::new(std::cell::Cell::new(false));
|
||||
|
||||
handle.serialize_text_fn.set_value(Some(Arc::new(move || {
|
||||
core_signal
|
||||
@@ -623,6 +629,34 @@ pub fn TerminalPanel(
|
||||
}
|
||||
});
|
||||
|
||||
let lifecycle_webgl_state = webgl_state.clone();
|
||||
let lifecycle_context_lost = webgl_context_lost.clone();
|
||||
Effect::new(move |prev: Option<Option<WebGlContextLifecycle>>| {
|
||||
drop(prev);
|
||||
|
||||
let renderer = options_signal.get().renderer;
|
||||
if renderer != Renderer::WebGl {
|
||||
lifecycle_context_lost.set(false);
|
||||
lifecycle_webgl_state.borrow_mut().take();
|
||||
return None;
|
||||
}
|
||||
let Some(canvas) = canvas_ref.get() else {
|
||||
return None;
|
||||
};
|
||||
let Some(canvas_el) = canvas.dyn_ref::<HtmlCanvasElement>() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
attach_webgl_context_lifecycle(
|
||||
canvas_el,
|
||||
lifecycle_webgl_state.clone(),
|
||||
lifecycle_context_lost.clone(),
|
||||
move || {
|
||||
let _ = render_tick.try_update(|t| *t += 1);
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
Effect::new(move |_| {
|
||||
let Some(on_selection_change) = on_selection_change else {
|
||||
return;
|
||||
@@ -815,6 +849,9 @@ pub fn TerminalPanel(
|
||||
let Some(canvas) = canvas_ref.get() else {
|
||||
return;
|
||||
};
|
||||
let Some(fallback_canvas) = canvas_fallback_ref.get() else {
|
||||
return;
|
||||
};
|
||||
let Some(element) = terminal_ref.get() else {
|
||||
return;
|
||||
};
|
||||
@@ -827,6 +864,9 @@ pub fn TerminalPanel(
|
||||
let Some(canvas_el) = canvas.dyn_ref::<HtmlCanvasElement>() else {
|
||||
return;
|
||||
};
|
||||
let Some(fallback_canvas_el) = fallback_canvas.dyn_ref::<HtmlCanvasElement>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let is_live = view_mode.get().is_live();
|
||||
let st = scroll_top.get();
|
||||
@@ -881,15 +921,35 @@ pub fn TerminalPanel(
|
||||
};
|
||||
match renderer {
|
||||
Renderer::WebGl => {
|
||||
if render_webgl(canvas_el, frame, &canvas_state, &webgl_state).is_err() {
|
||||
render_canvas_surface(RenderCanvasSurfaceRequest {
|
||||
if render_webgl(
|
||||
canvas_el,
|
||||
frame,
|
||||
&canvas_state,
|
||||
&webgl_state,
|
||||
&webgl_context_lost,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
set_canvas_surface_mode(
|
||||
canvas_el,
|
||||
fallback_canvas_el,
|
||||
webgl_surface_mode(true),
|
||||
);
|
||||
} else {
|
||||
set_canvas_surface_mode(
|
||||
canvas_el,
|
||||
fallback_canvas_el,
|
||||
webgl_surface_mode(false),
|
||||
);
|
||||
render_canvas_surface(RenderCanvasSurfaceRequest {
|
||||
canvas_el: fallback_canvas_el,
|
||||
frame,
|
||||
canvas_state: &canvas_state,
|
||||
canvas_state: &canvas_fallback_state,
|
||||
});
|
||||
}
|
||||
}
|
||||
Renderer::Canvas => {
|
||||
set_canvas_surface_mode(canvas_el, fallback_canvas_el, CanvasSurfaceMode::WebGl);
|
||||
render_canvas_surface(RenderCanvasSurfaceRequest {
|
||||
canvas_el,
|
||||
frame,
|
||||
@@ -1885,6 +1945,11 @@ pub fn TerminalPanel(
|
||||
node_ref=canvas_ref
|
||||
class="terminal-canvas"
|
||||
></canvas>
|
||||
<canvas
|
||||
node_ref=canvas_fallback_ref
|
||||
class="terminal-canvas"
|
||||
hidden
|
||||
></canvas>
|
||||
{move || {
|
||||
// Canvas-backed renderers: only the scroll sizer
|
||||
// and selection overlay are DOM; text/backgrounds
|
||||
|
||||
@@ -4,10 +4,11 @@ use std::rc::Rc;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::closure::Closure;
|
||||
use web_sys::js_sys::Float32Array;
|
||||
use web_sys::{
|
||||
CanvasRenderingContext2d, HtmlCanvasElement, WebGl2RenderingContext, WebGlBuffer, WebGlProgram,
|
||||
WebGlShader, WebGlTexture, WebGlUniformLocation,
|
||||
CanvasRenderingContext2d, Event, HtmlCanvasElement, WebGl2RenderingContext, WebGlBuffer,
|
||||
WebGlProgram, WebGlShader, WebGlTexture, WebGlUniformLocation,
|
||||
};
|
||||
|
||||
use super::component::{ResolvedTerminalOptions, TerminalCursorStyle};
|
||||
@@ -254,6 +255,115 @@ pub(super) fn render_canvas_surface(request: RenderCanvasSurfaceRequest<'_>) {
|
||||
render_canvas(&ctx, request.frame);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(super) enum CanvasSurfaceMode {
|
||||
WebGl,
|
||||
CanvasFallback,
|
||||
}
|
||||
|
||||
impl CanvasSurfaceMode {
|
||||
fn webgl_visible(self) -> bool {
|
||||
matches!(self, Self::WebGl)
|
||||
}
|
||||
|
||||
fn fallback_visible(self) -> bool {
|
||||
matches!(self, Self::CanvasFallback)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn webgl_surface_mode(rendered_with_webgl: bool) -> CanvasSurfaceMode {
|
||||
if rendered_with_webgl {
|
||||
CanvasSurfaceMode::WebGl
|
||||
} else {
|
||||
CanvasSurfaceMode::CanvasFallback
|
||||
}
|
||||
}
|
||||
|
||||
fn should_attempt_webgl_render(context_lost: bool) -> bool {
|
||||
!context_lost
|
||||
}
|
||||
|
||||
pub(super) fn set_canvas_surface_mode(
|
||||
webgl_canvas: &HtmlCanvasElement,
|
||||
fallback_canvas: &HtmlCanvasElement,
|
||||
mode: CanvasSurfaceMode,
|
||||
) {
|
||||
set_canvas_hidden(webgl_canvas, !mode.webgl_visible());
|
||||
set_canvas_hidden(fallback_canvas, !mode.fallback_visible());
|
||||
}
|
||||
|
||||
fn set_canvas_hidden(canvas: &HtmlCanvasElement, hidden: bool) {
|
||||
if hidden {
|
||||
let _ = canvas.set_attribute("hidden", "");
|
||||
} else {
|
||||
let _ = canvas.remove_attribute("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct WebGlContextLifecycle {
|
||||
canvas: HtmlCanvasElement,
|
||||
on_lost: Closure<dyn Fn(Event)>,
|
||||
on_restored: Closure<dyn Fn(Event)>,
|
||||
}
|
||||
|
||||
impl Drop for WebGlContextLifecycle {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.canvas.remove_event_listener_with_callback(
|
||||
"webglcontextlost",
|
||||
self.on_lost.as_ref().unchecked_ref(),
|
||||
);
|
||||
let _ = self.canvas.remove_event_listener_with_callback(
|
||||
"webglcontextrestored",
|
||||
self.on_restored.as_ref().unchecked_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn attach_webgl_context_lifecycle(
|
||||
canvas: &HtmlCanvasElement,
|
||||
webgl_state: Rc<std::cell::RefCell<Option<WebGlRendererState>>>,
|
||||
context_lost: Rc<std::cell::Cell<bool>>,
|
||||
render_tick: impl Fn() + 'static,
|
||||
) -> Option<WebGlContextLifecycle> {
|
||||
let lost_state = webgl_state.clone();
|
||||
let lost_flag = context_lost.clone();
|
||||
let on_lost = Closure::<dyn Fn(Event)>::new(move |event: Event| {
|
||||
event.prevent_default();
|
||||
lost_flag.set(true);
|
||||
lost_state.borrow_mut().take();
|
||||
});
|
||||
canvas
|
||||
.add_event_listener_with_callback("webglcontextlost", on_lost.as_ref().unchecked_ref())
|
||||
.ok()?;
|
||||
|
||||
let restored_state = webgl_state;
|
||||
let restored_flag = context_lost;
|
||||
let on_restored = Closure::<dyn Fn(Event)>::new(move |_| {
|
||||
restored_flag.set(false);
|
||||
restored_state.borrow_mut().take();
|
||||
render_tick();
|
||||
});
|
||||
if canvas
|
||||
.add_event_listener_with_callback(
|
||||
"webglcontextrestored",
|
||||
on_restored.as_ref().unchecked_ref(),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
let _ = canvas.remove_event_listener_with_callback(
|
||||
"webglcontextlost",
|
||||
on_lost.as_ref().unchecked_ref(),
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(WebGlContextLifecycle {
|
||||
canvas: canvas.clone(),
|
||||
on_lost,
|
||||
on_restored,
|
||||
})
|
||||
}
|
||||
|
||||
const WEBGL_VERTEX_SHADER: &str = r#"
|
||||
attribute vec2 a_position;
|
||||
attribute vec2 a_tex_coord;
|
||||
@@ -549,7 +659,11 @@ pub(super) fn render_webgl(
|
||||
frame: CanvasSurfaceFrame<'_>,
|
||||
canvas_state: &Rc<std::cell::RefCell<Option<CanvasDomState>>>,
|
||||
webgl_state: &Rc<std::cell::RefCell<Option<WebGlRendererState>>>,
|
||||
context_lost: &Rc<std::cell::Cell<bool>>,
|
||||
) -> Result<(), String> {
|
||||
if !should_attempt_webgl_render(context_lost.get()) {
|
||||
return Err("WebGL context is lost".to_owned());
|
||||
}
|
||||
sync_canvas_dom(canvas, frame.dom_state, canvas_state);
|
||||
let mut state_ref = webgl_state.borrow_mut();
|
||||
if state_ref.is_none() {
|
||||
@@ -558,6 +672,11 @@ pub(super) fn render_webgl(
|
||||
let state = state_ref
|
||||
.as_mut()
|
||||
.ok_or_else(|| "missing WebGL renderer state".to_owned())?;
|
||||
if state.gl.is_context_lost() {
|
||||
context_lost.set(true);
|
||||
state_ref.take();
|
||||
return Err("WebGL context is lost".to_owned());
|
||||
}
|
||||
|
||||
sync_canvas_bitmap(&state.source_canvas, frame.dom_state);
|
||||
let _ = state
|
||||
@@ -1218,6 +1337,22 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn webgl_context_loss_gates_render_attempts() {
|
||||
assert!(should_attempt_webgl_render(false));
|
||||
assert!(!should_attempt_webgl_render(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn webgl_surface_mode_selects_fallback_after_failed_render() {
|
||||
assert_eq!(webgl_surface_mode(true), CanvasSurfaceMode::WebGl);
|
||||
assert_eq!(webgl_surface_mode(false), CanvasSurfaceMode::CanvasFallback);
|
||||
assert!(CanvasSurfaceMode::WebGl.webgl_visible());
|
||||
assert!(!CanvasSurfaceMode::WebGl.fallback_visible());
|
||||
assert!(!CanvasSurfaceMode::CanvasFallback.webgl_visible());
|
||||
assert!(CanvasSurfaceMode::CanvasFallback.fallback_visible());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_segment_glyphs_tracks_width_and_color() {
|
||||
let segment = TerminalSegment {
|
||||
|
||||
Reference in New Issue
Block a user