use std::ffi::c_void;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::{Duration, Instant};
use egl::{EGLContext, EGLDisplay, EGLSurface};
use ndk::native_window::NativeWindow;
use glow::HasContext;
use egui::{
Event, PointerButton, RawInput, Rect, TouchDeviceId, TouchId, TouchPhase, Vec2, ViewportInfo,
ViewportId,
};
use crate::kotlin::maybe_request_native_editor;
use crate::state::{AppState, TouchAction, TEXT_BRIDGE};
pub(crate) fn render_loop(state: Arc<AppState>) {
'outer: loop {
while state.running.load(Ordering::SeqCst) {
let window = loop {
if !state.running.load(Ordering::SeqCst) {
break None;
}
if let Some(window) = state.window.lock().unwrap().clone() {
break Some(window);
}
let guard = state.wake_lock.lock().unwrap();
if !state.running.load(Ordering::SeqCst) {
break None;
}
let guard = state.wake.wait(guard).unwrap();
drop(guard);
};
let Some(window) = window else { break };
match run_render_session(&state, &window) {
Ok(()) => {}
Err(e) => {
log::error!("render session error: {e}");
std::thread::sleep(Duration::from_millis(200));
}
}
}
let mut guard = state.render_thread.lock().unwrap();
if state.running.load(Ordering::SeqCst) {
drop(guard);
continue 'outer;
}
guard.take(); break;
}
}
fn run_render_session(state: &Arc<AppState>, window: &NativeWindow) -> Result<(), String> {
let session = init_egl(window)?;
let gl = unsafe {
glow::Context::from_loader_function(|name| egl::get_proc_address(name) as *const c_void)
};
let gl = Arc::new(gl);
let mut painter = egui_glow::Painter::new(
gl.clone(),
"",
Some(egui_glow::ShaderVersion::Es100),
false,
)
.map_err(|e| format!("egui_glow painter: {e}"))?;
let egui_ctx = egui::Context::default();
egui_ctx.set_zoom_factor(1.0);
if let Some(saved) = state.egui_memory.lock().unwrap().as_ref() {
egui_ctx.memory_mut(|m| *m = saved.clone());
}
egui_ctx.set_fonts(font_definitions().clone());
egui_ctx.set_zoom_factor(1.0);
*state.egui_ctx.lock().unwrap() = Some(egui_ctx.clone());
{
let state = state.clone();
egui_ctx.set_request_repaint_callback(move |info| {
let deadline = Instant::now() + info.delay;
{
let mut slot = state.repaint_deadline.lock().unwrap();
if slot.map_or(true, |d| deadline < d) {
*slot = Some(deadline);
}
}
state.wake_renderer();
});
}
let mut frame_count: u64 = 0;
let mut first_frame = true;
let mut rendered_valid_frame = false;
static PROCESS_START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
let process_time = || PROCESS_START.get_or_init(Instant::now).elapsed().as_secs_f64();
loop {
if !state.running.load(Ordering::SeqCst) {
break;
}
if state.take_rebind() {
break;
}
let (w_px, h_px) = *state.size_px.lock().unwrap();
let density = *state.density.lock().unwrap();
if w_px == 0 || h_px == 0 || density <= 0.0 {
std::thread::sleep(Duration::from_millis(50));
continue;
}
if h_px > w_px {
std::thread::sleep(Duration::from_millis(50));
continue;
}
if !first_frame && !wait_for_render_reason(state, &egui_ctx) {
break;
}
first_frame = false;
let mut raw_input = RawInput {
screen_rect: Some(Rect::from_min_size(
egui::Pos2::ZERO,
Vec2::new(w_px as f32 / density, h_px as f32 / density),
)),
time: Some(process_time()),
viewports: [(
ViewportId::ROOT,
ViewportInfo {
native_pixels_per_point: Some(density),
..Default::default()
},
)]
.into_iter()
.collect(),
events: Vec::new(),
..Default::default()
};
if let Some(touch) = state.touches.lock().unwrap().pop_front() {
let pos = egui::pos2(touch.x, touch.y);
let phase = match touch.action {
TouchAction::Down => TouchPhase::Start,
TouchAction::Move => TouchPhase::Move,
TouchAction::Up => TouchPhase::End,
};
raw_input.events.push(Event::Touch {
device_id: TouchDeviceId(0),
id: TouchId(0),
phase,
pos,
force: None,
});
match touch.action {
TouchAction::Down => raw_input.events.push(Event::PointerButton {
pos,
button: PointerButton::Primary,
pressed: true,
modifiers: egui::Modifiers::default(),
}),
TouchAction::Move => raw_input.events.push(Event::PointerMoved(pos)),
TouchAction::Up => {
raw_input.events.push(Event::PointerButton {
pos,
button: PointerButton::Primary,
pressed: false,
modifiers: egui::Modifiers::default(),
});
raw_input.events.push(Event::PointerGone);
}
}
}
let extra_events = std::mem::take(&mut *state.extra_events.lock().unwrap());
raw_input.events.extend(extra_events);
let mut full_output = egui_ctx.run_ui(raw_input, |ui| {
if let Some(render_fn) = crate::UI_RENDERER.lock().unwrap().as_mut() {
render_fn(ui);
}
});
maybe_request_native_editor(state);
let pixels_per_point = full_output.pixels_per_point;
let clipped = egui_ctx.tessellate(full_output.shapes, pixels_per_point);
unsafe {
gl.clear_color(0.0, 0.0, 0.0, 0.0);
gl.clear(glow::COLOR_BUFFER_BIT);
}
painter.paint_and_update_textures(
[w_px, h_px],
pixels_per_point,
&clipped,
&mut full_output.textures_delta,
);
if !egl::swap_buffers(session.display, session.surface) {
log::warn!("eglSwapBuffers failed - surface lost");
break;
}
rendered_valid_frame = true;
frame_count += 1;
if frame_count % 120 == 0 {
}
}
if rendered_valid_frame {
*state.egui_memory.lock().unwrap() = Some(egui_ctx.memory(|m| m.clone()));
}
painter.destroy();
egl::make_current(
session.display,
egl::EGL_NO_SURFACE,
egl::EGL_NO_SURFACE,
egl::EGL_NO_CONTEXT,
);
*state.egui_ctx.lock().unwrap() = None;
egl::destroy_surface(session.display, session.surface);
egl::destroy_context(session.display, session.context);
egl::terminate(session.display);
Ok(())
}
fn wait_for_render_reason(state: &Arc<AppState>, egui_ctx: &egui::Context) -> bool {
loop {
if !state.running.load(Ordering::SeqCst) {
return false;
}
if state.take_rebind() {
return false;
}
if has_pending_input(state) || egui_ctx.requested_repaint_last_pass() {
return true;
}
let guard = state.wake_lock.lock().unwrap();
if !state.running.load(Ordering::SeqCst) {
return false;
}
if state.take_rebind() {
return false;
}
if has_pending_input(state) || egui_ctx.requested_repaint_last_pass() {
return true;
}
let deadline = *state.repaint_deadline.lock().unwrap();
match deadline {
Some(d) => {
let now = Instant::now();
if now >= d {
*state.repaint_deadline.lock().unwrap() = None;
return true;
}
let (guard, _) = state.wake.wait_timeout(guard, d - now).unwrap();
drop(guard);
}
None => {
let guard = state.wake.wait(guard).unwrap();
drop(guard);
}
}
}
}
fn has_pending_input(state: &AppState) -> bool {
!state.touches.lock().unwrap().is_empty()
|| !state.extra_events.lock().unwrap().is_empty()
|| !TEXT_BRIDGE.lock().unwrap().results.is_empty()
}
fn init_egl(window: &NativeWindow) -> Result<EglSession, String> {
let display = egl::get_display(egl::EGL_DEFAULT_DISPLAY)
.ok_or_else(|| "eglGetDisplay failed".to_owned())?;
let mut major = 0;
let mut minor = 0;
if !egl::initialize(display, &mut major, &mut minor) {
return Err("eglInitialize failed".to_owned());
}
let attribs = [
egl::EGL_SURFACE_TYPE,
egl::EGL_WINDOW_BIT,
egl::EGL_RENDERABLE_TYPE,
egl::EGL_OPENGL_ES2_BIT,
egl::EGL_RED_SIZE,
8,
egl::EGL_GREEN_SIZE,
8,
egl::EGL_BLUE_SIZE,
8,
egl::EGL_ALPHA_SIZE,
8,
egl::EGL_NONE,
];
let config = egl::choose_config(display, &attribs, 1)
.ok_or_else(|| "eglChooseConfig failed".to_owned())?;
let context = egl::create_context(
display,
config,
egl::EGL_NO_CONTEXT,
&[egl::EGL_CONTEXT_CLIENT_VERSION, 2, egl::EGL_NONE],
)
.ok_or_else(|| "eglCreateContext failed".to_owned())?;
let surface = egl::create_window_surface(
display,
config,
window.ptr().as_ptr() as egl::EGLNativeWindowType,
&[],
)
.ok_or_else(|| "eglCreateWindowSurface failed".to_owned())?;
if !egl::make_current(display, surface, surface, context) {
return Err("eglMakeCurrent failed".to_owned());
}
egl::swap_interval(display, 1);
Ok(EglSession {
display,
surface,
context,
})
}
struct EglSession {
display: EGLDisplay,
surface: EGLSurface,
context: EGLContext,
}
fn font_definitions() -> &'static egui::FontDefinitions {
static FONTS: std::sync::OnceLock<egui::FontDefinitions> = std::sync::OnceLock::new();
FONTS.get_or_init(build_font_definitions)
}
fn build_font_definitions() -> egui::FontDefinitions {
let mut fonts = egui::FontDefinitions::default();
match load_system_cjk_font() {
Some((bytes, index)) => {
let mut data = egui::FontData::from_owned(bytes);
data.index = index;
fonts
.font_data
.insert("system_cjk".to_owned(), std::sync::Arc::new(data));
for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] {
fonts
.families
.entry(family)
.or_default()
.insert(0, "system_cjk".to_owned());
}
}
None => log::error!("renderer: 未找到可用的系统 CJK 字体,UI 文字将无法显示"),
}
fonts
}
fn load_system_cjk_font() -> Option<(Vec<u8>, u32)> {
const CANDIDATES: [(&str, u32); 4] = [
("/system/fonts/NotoSansCJK-Regular.ttc", 2),
("/system/fonts/NotoSansSC-Regular.otf", 0),
("/system/fonts/NotoSansCJKsc-Regular.otf", 0),
("/system/fonts/DroidSansFallback.ttf", 0),
];
for (path, want) in CANDIDATES {
match std::fs::read(path) {
Ok(bytes) => {
let index = match ttc_face_count(&bytes) {
Some(count) if want < count => want,
Some(_) => 0,
None => 0,
};
return Some((bytes, index));
}
Err(_) => {}
}
}
None
}
fn ttc_face_count(bytes: &[u8]) -> Option<u32> {
if bytes.len() >= 12 && &bytes[0..4] == b"ttcf" {
Some(u32::from_be_bytes(bytes[8..12].try_into().ok()?))
} else {
None
}
}