feat(terminal): recover webgl renderer context
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
# Terminal Handoff
|
# Terminal Handoff
|
||||||
|
|
||||||
|
> 说明:本文档保留早期 scrollback 排查背景。当前终端实现已经进入 WebGL 默认渲染器、addon parity 与 P9 renderer 工程化阶段;最新状态请优先看 `TERMINAL_ROADMAP.md`。
|
||||||
|
|
||||||
本文档用于把 `base-path-demo` 当前浏览器终端实现的状态、已验证结论、未解决问题和下一步建议整理清楚,方便后续继续开发。
|
本文档用于把 `base-path-demo` 当前浏览器终端实现的状态、已验证结论、未解决问题和下一步建议整理清楚,方便后续继续开发。
|
||||||
|
|
||||||
## 1. 当前目标
|
## 1. 当前目标
|
||||||
@@ -278,4 +280,3 @@ pub struct TerminalBufferModel {
|
|||||||
- 滚回底部是否恢复 prompt
|
- 滚回底部是否恢复 prompt
|
||||||
- history 模式输入后是否回到 live
|
- history 模式输入后是否回到 live
|
||||||
- resize 后历史视图是否稳定
|
- resize 后历史视图是否稳定
|
||||||
|
|
||||||
|
|||||||
@@ -231,7 +231,11 @@
|
|||||||
### P9 — WebGL Renderer 工程化深化
|
### P9 — WebGL Renderer 工程化深化
|
||||||
|
|
||||||
**计划实现**:
|
**计划实现**:
|
||||||
1. WebGL context loss/recovery 与 atlas 重建。
|
1. **WebGL context loss/recovery 与 atlas 重建** ✅
|
||||||
|
- 监听 `webglcontextlost` / `webglcontextrestored`,lost 时阻止默认行为并清空 `WebGlRendererState`。
|
||||||
|
- context lost 或 WebGL 初始化失败时显示独立 Canvas 2D fallback surface,避免同一 canvas 上 WebGL lost 后无法获取 2D context 导致白屏。
|
||||||
|
- restore 后清空旧 state 并触发重绘,下一帧重建 shader/program/buffer/texture/glyph atlas。
|
||||||
|
- context lifecycle listener 由 `WebGlContextLifecycle` 持有,组件卸载或 renderer 切换时自动移除。
|
||||||
2. 渲染性能 benchmark 和自动截图 / pixel smoke test。
|
2. 渲染性能 benchmark 和自动截图 / pixel smoke test。
|
||||||
3. Canvas 2D / WebGL runtime switch。
|
3. Canvas 2D / WebGL runtime switch。
|
||||||
4. 与 Canvas 2D 做 benchmark 对比,按 viewport size / output rate 自动选择 renderer。
|
4. 与 Canvas 2D 做 benchmark 对比,按 viewport size / output rate 自动选择 renderer。
|
||||||
|
|||||||
@@ -27,8 +27,10 @@ use super::mouse::{
|
|||||||
};
|
};
|
||||||
use super::protocol::{ClientTerminalMessage, ServerTerminalMessage};
|
use super::protocol::{ClientTerminalMessage, ServerTerminalMessage};
|
||||||
use super::renderer::{
|
use super::renderer::{
|
||||||
CanvasDomState, CanvasPlanInput, CanvasSurfaceFrame, RenderCanvasSurfaceRequest,
|
CanvasDomState, CanvasPlanInput, CanvasSurfaceFrame, CanvasSurfaceMode,
|
||||||
WebGlRendererState, canvas_render_plan, render_canvas_surface, render_webgl,
|
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::scrollback::ViewMode;
|
||||||
use super::selection::{Selection, SelectionRect, selection_rects};
|
use super::selection::{Selection, SelectionRect, selection_rects};
|
||||||
@@ -472,6 +474,7 @@ pub fn TerminalPanel(
|
|||||||
let terminal_ref = NodeRef::<html::Div>::new();
|
let terminal_ref = NodeRef::<html::Div>::new();
|
||||||
let ime_ref = NodeRef::<html::Textarea>::new();
|
let ime_ref = NodeRef::<html::Textarea>::new();
|
||||||
let canvas_ref = NodeRef::<html::Canvas>::new();
|
let canvas_ref = NodeRef::<html::Canvas>::new();
|
||||||
|
let canvas_fallback_ref = NodeRef::<html::Canvas>::new();
|
||||||
let measure_ref = NodeRef::<html::Span>::new();
|
let measure_ref = NodeRef::<html::Span>::new();
|
||||||
let connection_status = RwSignal::new("Connecting".to_owned());
|
let connection_status = RwSignal::new("Connecting".to_owned());
|
||||||
let ws_signal = RwSignal::new(None::<WebSocket>);
|
let ws_signal = RwSignal::new(None::<WebSocket>);
|
||||||
@@ -525,8 +528,11 @@ pub fn TerminalPanel(
|
|||||||
Rc::new(std::cell::Cell::new(None));
|
Rc::new(std::cell::Cell::new(None));
|
||||||
let canvas_state: Rc<std::cell::RefCell<Option<CanvasDomState>>> =
|
let canvas_state: Rc<std::cell::RefCell<Option<CanvasDomState>>> =
|
||||||
Rc::new(std::cell::RefCell::new(None));
|
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>>> =
|
let webgl_state: Rc<std::cell::RefCell<Option<WebGlRendererState>>> =
|
||||||
Rc::new(std::cell::RefCell::new(None));
|
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 || {
|
handle.serialize_text_fn.set_value(Some(Arc::new(move || {
|
||||||
core_signal
|
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 |_| {
|
Effect::new(move |_| {
|
||||||
let Some(on_selection_change) = on_selection_change else {
|
let Some(on_selection_change) = on_selection_change else {
|
||||||
return;
|
return;
|
||||||
@@ -815,6 +849,9 @@ pub fn TerminalPanel(
|
|||||||
let Some(canvas) = canvas_ref.get() else {
|
let Some(canvas) = canvas_ref.get() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let Some(fallback_canvas) = canvas_fallback_ref.get() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
let Some(element) = terminal_ref.get() else {
|
let Some(element) = terminal_ref.get() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -827,6 +864,9 @@ pub fn TerminalPanel(
|
|||||||
let Some(canvas_el) = canvas.dyn_ref::<HtmlCanvasElement>() else {
|
let Some(canvas_el) = canvas.dyn_ref::<HtmlCanvasElement>() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let Some(fallback_canvas_el) = fallback_canvas.dyn_ref::<HtmlCanvasElement>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let is_live = view_mode.get().is_live();
|
let is_live = view_mode.get().is_live();
|
||||||
let st = scroll_top.get();
|
let st = scroll_top.get();
|
||||||
@@ -881,15 +921,35 @@ pub fn TerminalPanel(
|
|||||||
};
|
};
|
||||||
match renderer {
|
match renderer {
|
||||||
Renderer::WebGl => {
|
Renderer::WebGl => {
|
||||||
if render_webgl(canvas_el, frame, &canvas_state, &webgl_state).is_err() {
|
if render_webgl(
|
||||||
render_canvas_surface(RenderCanvasSurfaceRequest {
|
|
||||||
canvas_el,
|
canvas_el,
|
||||||
frame,
|
frame,
|
||||||
canvas_state: &canvas_state,
|
&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_fallback_state,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Renderer::Canvas => {
|
Renderer::Canvas => {
|
||||||
|
set_canvas_surface_mode(canvas_el, fallback_canvas_el, CanvasSurfaceMode::WebGl);
|
||||||
render_canvas_surface(RenderCanvasSurfaceRequest {
|
render_canvas_surface(RenderCanvasSurfaceRequest {
|
||||||
canvas_el,
|
canvas_el,
|
||||||
frame,
|
frame,
|
||||||
@@ -1885,6 +1945,11 @@ pub fn TerminalPanel(
|
|||||||
node_ref=canvas_ref
|
node_ref=canvas_ref
|
||||||
class="terminal-canvas"
|
class="terminal-canvas"
|
||||||
></canvas>
|
></canvas>
|
||||||
|
<canvas
|
||||||
|
node_ref=canvas_fallback_ref
|
||||||
|
class="terminal-canvas"
|
||||||
|
hidden
|
||||||
|
></canvas>
|
||||||
{move || {
|
{move || {
|
||||||
// Canvas-backed renderers: only the scroll sizer
|
// Canvas-backed renderers: only the scroll sizer
|
||||||
// and selection overlay are DOM; text/backgrounds
|
// and selection overlay are DOM; text/backgrounds
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ use std::rc::Rc;
|
|||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
|
use wasm_bindgen::closure::Closure;
|
||||||
use web_sys::js_sys::Float32Array;
|
use web_sys::js_sys::Float32Array;
|
||||||
use web_sys::{
|
use web_sys::{
|
||||||
CanvasRenderingContext2d, HtmlCanvasElement, WebGl2RenderingContext, WebGlBuffer, WebGlProgram,
|
CanvasRenderingContext2d, Event, HtmlCanvasElement, WebGl2RenderingContext, WebGlBuffer,
|
||||||
WebGlShader, WebGlTexture, WebGlUniformLocation,
|
WebGlProgram, WebGlShader, WebGlTexture, WebGlUniformLocation,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::component::{ResolvedTerminalOptions, TerminalCursorStyle};
|
use super::component::{ResolvedTerminalOptions, TerminalCursorStyle};
|
||||||
@@ -254,6 +255,115 @@ pub(super) fn render_canvas_surface(request: RenderCanvasSurfaceRequest<'_>) {
|
|||||||
render_canvas(&ctx, request.frame);
|
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#"
|
const WEBGL_VERTEX_SHADER: &str = r#"
|
||||||
attribute vec2 a_position;
|
attribute vec2 a_position;
|
||||||
attribute vec2 a_tex_coord;
|
attribute vec2 a_tex_coord;
|
||||||
@@ -549,7 +659,11 @@ pub(super) fn render_webgl(
|
|||||||
frame: CanvasSurfaceFrame<'_>,
|
frame: CanvasSurfaceFrame<'_>,
|
||||||
canvas_state: &Rc<std::cell::RefCell<Option<CanvasDomState>>>,
|
canvas_state: &Rc<std::cell::RefCell<Option<CanvasDomState>>>,
|
||||||
webgl_state: &Rc<std::cell::RefCell<Option<WebGlRendererState>>>,
|
webgl_state: &Rc<std::cell::RefCell<Option<WebGlRendererState>>>,
|
||||||
|
context_lost: &Rc<std::cell::Cell<bool>>,
|
||||||
) -> Result<(), String> {
|
) -> 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);
|
sync_canvas_dom(canvas, frame.dom_state, canvas_state);
|
||||||
let mut state_ref = webgl_state.borrow_mut();
|
let mut state_ref = webgl_state.borrow_mut();
|
||||||
if state_ref.is_none() {
|
if state_ref.is_none() {
|
||||||
@@ -558,6 +672,11 @@ pub(super) fn render_webgl(
|
|||||||
let state = state_ref
|
let state = state_ref
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.ok_or_else(|| "missing WebGL renderer state".to_owned())?;
|
.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);
|
sync_canvas_bitmap(&state.source_canvas, frame.dom_state);
|
||||||
let _ = 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]
|
#[test]
|
||||||
fn split_segment_glyphs_tracks_width_and_color() {
|
fn split_segment_glyphs_tracks_width_and_color() {
|
||||||
let segment = TerminalSegment {
|
let segment = TerminalSegment {
|
||||||
|
|||||||
Reference in New Issue
Block a user