#define AL_LOG_SECTION "screen" //#define AL_LOG_ENABLE_TRACE #include #include #include #include #include "view.h" #include "screen.h" #define SCREEN_INVALID_TS ((u64)-1) #define SCREEN_MOD1(scr) ((scr)->window->held_modifiers & (STELA_MOD_LCONTROL | STELA_MOD_RCONTROL)) #define SCREEN_MOD2(scr) ((scr)->window->held_modifiers & (STELA_MOD_LSHIFT | STELA_MOD_RSHIFT)) #define SCREEN_MOUSE_MOD(scr) ((scr)->flags & CAMU_SCREEN_MOUSE_MOD) #define SCREEN_IS_DRAGGING(scr) ((scr)->flags & CAMU_SCREEN_DRAGGING) #define SCREEN_IS_ZOOMING(scr) ((scr)->flags & CAMU_SCREEN_ZOOMING) #define SCREEN_IS_FROZEN(scr) ((scr)->flags & CAMU_SCREEN_FROZEN) #define SCREEN_ZOOM_MODE(scr, mode) ((scr)->flags & mode) #define SCREEN_DEFAULT_CURSOR(scr) ((scr->fullscreen || scr->vr_emulation) ? STELA_CURSOR_HIDDEN : STELA_CURSOR_NORMAL) #define SCREEN_LAST_CLICK_WITHIN(ns) \ (((scr)->last_click_ts != SCREEN_INVALID_TS) && (nn_get_timestamp() - (scr)->last_click_ts <= ns)) static void should_close_callback(void *userdata) { struct camu_screen *scr = (struct camu_screen *)userdata; scr->window->close(scr->window); scr->callback(scr->userdata, CAMU_SCREEN_CLOSE, NULL); } static void render_callback(void *userdata) { struct camu_screen *scr = (struct camu_screen *)userdata; bool force; if (scr->renderer && camu_screen_tick(scr, &force)) { scr->renderer->render(scr->renderer, scr, force); } } static void do_resize(struct camu_screen *scr, u32 width, u32 height) { if (scr->renderer) { scr->renderer->resize(scr->renderer, &width, &height); } al_assert(width > 0 && height > 0); scr->width = width; scr->height = height; struct camu_screen_video *video; al_array_foreach_ptr(scr->videos, i, video) { camu_view_calculate(&video->view, scr->width, scr->height); } // No buffers resize. atomic_add(u32)(&scr->force_refresh, 1, AL_ATOMIC_RELAXED); } static void resize_callback(void *userdata, u32 width, u32 height) { struct camu_screen *scr = (struct camu_screen *)userdata; do_resize(scr, width, height); } static struct camu_view *get_view_from_mouse_pos(struct camu_screen *scr) { struct camu_view *view = NULL; struct camu_screen_video *video; al_array_foreach_ptr(scr->videos, i, video) { if (!view) { view = &video->view; continue; } if (view->zindex < video->view.zindex) { view = &video->view; } } return view; } static void seek_to_percent_at_pointer(struct camu_screen *scr, f64 x) { f64 percent = x / scr->width; percent = CLAMP(percent, 0.0, 100.0); scr->callback(scr->userdata, CAMU_SCREEN_PERCENT_SEEK, &percent); } static bool pointer_pos_callback(void *userdata, f64 x, f64 y) { struct camu_screen *scr = (struct camu_screen *)userdata; struct camu_view *view = get_view_from_mouse_pos(scr); bool queue_refresh = false; if (SCREEN_IS_DRAGGING(scr)) { #ifdef CAMU_SCREEN_DRAG_SEEK u64 now = nn_get_timestamp(); if (view && now - scr->last_seek_ts > 100000) { seek_to_percent_at_pointer(scr, x); scr->last_seek_ts = now; } #else if (view) { if (SCREEN_ZOOM_MODE(scr, CAMU_SCREEN_ZOOM_PAN_SIMPLE)) { f64 dx = x - scr->last_pointer_x; f64 dy = y - scr->last_pointer_y; if (camu_view_pan_simple(view, scr->width, scr->height, dx, dy)) { view->mode = CAMU_VIEW_DETACHED; queue_refresh = true; } if (fabs(dx) + fabs(dy) > 6.0) { scr->last_click_ts = SCREEN_INVALID_TS; } } } #endif } scr->last_pointer_x = x; scr->last_pointer_y = y; return queue_refresh; } static bool scroll_callback(void *userdata, f64 y); static bool touch_callback(void *userdata, s32 index, u8 phase, f64 x, f64 y) { // Rough testing stuff. #define distance(x1, x2, y1, y2) sqrt(((x2 - x1) * (x2 - x1)) + ((y2 - y1) * (y2 - y1))) struct camu_screen *scr = (struct camu_screen *)userdata; switch (phase) { case STELA_TOUCH_BEGAN: { if (index == 0) { scr->last_touch_x[0] = x; scr->last_touch_y[0] = y; } else if (index == 1) { scr->last_touch_x[1] = x; scr->last_touch_y[1] = y; } if (!SCREEN_IS_DRAGGING(scr)) { scr->flags |= CAMU_SCREEN_DRAGGING; scr->last_click_ts = nn_get_timestamp(); } else if (!SCREEN_IS_ZOOMING(scr)) { scr->flags |= CAMU_SCREEN_ZOOMING; f64 lx = scr->last_touch_x[0], lx2 = scr->last_touch_x[1]; f64 ly = scr->last_touch_y[0], ly2 = scr->last_touch_y[1]; scr->last_distance = distance(lx, lx2, ly, ly2); scr->midpoint_x = (lx + lx2) / 2.0; scr->midpoint_y = (ly + ly2) / 2.0; } else if (!SCREEN_IS_FROZEN(scr)) { scr->flags |= CAMU_SCREEN_FROZEN; struct camu_view *view = get_view_from_mouse_pos(scr); if (view) { view->mode = CAMU_DEFAULT_VIEW; camu_view_calculate(view, scr->width, scr->height); } } break; } case STELA_TOUCH_MOVED: { // @TODO: Track current index 1 and 2. On release search for next non-0 index in mask. if (SCREEN_IS_FROZEN(scr)) { return false; } else if (SCREEN_IS_ZOOMING(scr)) { if (index == 0) { scr->last_touch_x[0] = x; scr->last_touch_y[0] = y; } else if (index == 1) { scr->last_touch_x[1] = x; scr->last_touch_y[1] = y; } f64 lx = scr->last_touch_x[0], lx2 = scr->last_touch_x[1]; f64 ly = scr->last_touch_y[0], ly2 = scr->last_touch_y[1]; f64 dist = distance(lx, lx2, ly, ly2); f64 mx = x = (lx + lx2) / 2.0, my = (ly + ly2) / 2.0; bool refresh = false; struct camu_view *view = get_view_from_mouse_pos(scr); if (view) { refresh |= camu_view_pan_simple(view, scr->width, scr->height, mx - scr->midpoint_x, my - scr->midpoint_y); } scr->midpoint_x = mx; scr->midpoint_y = my; scr->last_pointer_x = scr->midpoint_x; scr->last_pointer_y = scr->midpoint_y; refresh |= scroll_callback(scr, (dist - scr->last_distance) / 120.0); scr->last_distance = dist; return refresh; } else if (SCREEN_IS_DRAGGING(scr)) { if (index == 0) { scr->last_pointer_x = scr->last_touch_x[0]; scr->last_pointer_y = scr->last_touch_y[0]; } else if (index == 1) { scr->last_pointer_x = scr->last_touch_x[1]; scr->last_pointer_y = scr->last_touch_y[1]; } bool refresh = pointer_pos_callback(scr, x, y); if (index == 0) { scr->last_touch_x[0] = x; scr->last_touch_y[0] = y; } else if (index == 1) { scr->last_touch_x[1] = x; scr->last_touch_y[1] = y; } return refresh; } break; } case STELA_TOUCH_ENDED: { if (SCREEN_IS_FROZEN(scr)) { scr->flags &= ~CAMU_SCREEN_FROZEN; seek_to_percent_at_pointer(scr, x); } else if (SCREEN_IS_ZOOMING(scr)) { scr->flags &= ~CAMU_SCREEN_ZOOMING; } else if (SCREEN_IS_DRAGGING(scr)) { scr->flags &= ~CAMU_SCREEN_DRAGGING; if (SCREEN_LAST_CLICK_WITHIN(200000)) { f64 third = scr->width / 3.0; if (x >= third && x <= third * 2.0) { scr->callback(scr->userdata, CAMU_SCREEN_TOGGLE_PAUSE, NULL); } else { if (scr->touch_mode) { #if CAMU_HAVE_SUBTITLES scr->subtitles_enabled = !scr->subtitles_enabled; #endif } else { s32 n = (x >= third) ? 1 : -1; scr->callback(scr->userdata, CAMU_SCREEN_SKIP, &n); } } } } break; } } return false; #undef distance } static bool mouse_button_callback(void *userdata, u8 state, u8 button) { struct camu_screen *scr = (struct camu_screen *)userdata; switch (button) { case STELA_MOUSE1: switch (state) { case STELA_BUTTON_PRESSED: scr->flags |= CAMU_SCREEN_DRAGGING; scr->window->set_cursor(scr->window, STELA_CURSOR_MOVE); scr->last_click_ts = nn_get_timestamp(); break; case STELA_BUTTON_RELEASED: scr->flags &= ~CAMU_SCREEN_DRAGGING; scr->window->set_cursor(scr->window, SCREEN_MOUSE_MOD(scr) ? STELA_CURSOR_CROSSHAIR : SCREEN_DEFAULT_CURSOR(scr)); if (SCREEN_LAST_CLICK_WITHIN(200000)) { if (SCREEN_MOD1(scr)) { seek_to_percent_at_pointer(scr, scr->last_pointer_x); } else { s32 n = (scr->last_pointer_x >= scr->width / 2.0) ? 1 : -1; scr->callback(scr->userdata, CAMU_SCREEN_SKIP, &n); } } break; } break; case STELA_MOUSE2: { switch (state) { case STELA_BUTTON_PRESSED: scr->flags |= CAMU_SCREEN_MOUSE_MOD; if (scr->fullscreen && !SCREEN_IS_DRAGGING(scr)) { scr->window->set_cursor(scr->window, STELA_CURSOR_CROSSHAIR); } break; case STELA_BUTTON_RELEASED: scr->flags &= ~CAMU_SCREEN_MOUSE_MOD; if (!SCREEN_IS_DRAGGING(scr)) { scr->window->set_cursor(scr->window, SCREEN_DEFAULT_CURSOR(scr)); } break; } break; } case STELA_MOUSE3: { switch (state) { case STELA_BUTTON_RELEASED: seek_to_percent_at_pointer(scr, scr->last_pointer_x); break; } break; } default: break; } return false; } bool scroll_callback(void *userdata, f64 y) { struct camu_screen *scr = (struct camu_screen *)userdata; if (SCREEN_MOD1(scr)) { f32 change = (f32)stl_scale_scroll(y, 40.0); scr->callback(scr->userdata, CAMU_SCREEN_OFFSET_VOLUME, &change); } else { struct camu_view *view = get_view_from_mouse_pos(scr); if (view) { if (SCREEN_ZOOM_MODE(scr, CAMU_SCREEN_ZOOM_PAN_SIMPLE)) { if (camu_view_zoom_simple(view, scr->width, scr->height, scr->last_pointer_x, scr->last_pointer_y, stl_scale_scroll(y, 15.0))) { view->mode = CAMU_VIEW_DETACHED; return true; } } else if (SCREEN_ZOOM_MODE(scr, CAMU_SCREEN_ZOOM_FOV)) { camu_view_zoom_fov(view, stl_scale_scroll(y, 20.0)); } } } return false; } static bool key_callback(void *userdata, u8 state, u16 button) { struct camu_screen *scr = (struct camu_screen *)userdata; switch (state) { case STELA_BUTTON_PRESSED: switch (button) { case STELA_KEY_Q: scr->window->close(scr->window); scr->callback(scr->userdata, CAMU_SCREEN_CLOSE, NULL); break; case STELA_KEY_RIGHT: if (SCREEN_MOD1(scr)) { f64 plus_ten_seconds = 10.0; scr->callback(scr->userdata, CAMU_SCREEN_RELATIVE_SEEK, &plus_ten_seconds); break; } // fallthrough case STELA_KEY_TAB: case STELA_KEY_D: { s32 n = 1; scr->callback(scr->userdata, CAMU_SCREEN_SKIP, &n); break; } case STELA_KEY_LEFT: if (SCREEN_MOD1(scr)) { if (SCREEN_MOD2(scr)) { f64 zero = 0.0; scr->callback(scr->userdata, CAMU_SCREEN_PERCENT_SEEK, &zero); } else { f64 minus_ten_seconds = -10.0; scr->callback(scr->userdata, CAMU_SCREEN_RELATIVE_SEEK, &minus_ten_seconds); } break; } // fallthrough case STELA_KEY_A: { s32 n = -1; scr->callback(scr->userdata, CAMU_SCREEN_SKIP, &n); break; } case STELA_KEY_SPACE: scr->callback(scr->userdata, CAMU_SCREEN_TOGGLE_PAUSE, NULL); break; case STELA_KEY_S: if (SCREEN_MOD1(scr)) { scr->callback(scr->userdata, CAMU_SCREEN_SHUFFLE, NULL); } else { #ifdef CAMU_HAVE_SUBTITLES scr->subtitles_enabled = !scr->subtitles_enabled; log_info(scr->subtitles_enabled ? "Subtitles enabled." : "Subtitles disabled."); return true; #endif } break; case STELA_KEY_E: if (scr->vr_emulation) { if (scr->vr_emulation == CAMU_VR_180) { scr->vr_left_eye = !scr->vr_left_eye; log_info(scr->vr_left_eye ? "Left eye." : "Right eye."); } } else { scr->callback(scr->userdata, CAMU_SCREEN_RESEEK, NULL); } break; case STELA_KEY_M: { f32 volume = SCREEN_MOD1(scr) ? 1.f : -1.f; scr->callback(scr->userdata, CAMU_SCREEN_SET_VOLUME, &volume); break; } case STELA_KEY_0: scr->callback(scr->userdata, CAMU_SCREEN_STATUS, NULL); break; #ifdef CAMU_SCREEN_DEBUG_KEY case STELA_KEY_P: scr->callback(scr->userdata, CAMU_SCREEN_DEBUG, NULL); break; #endif case STELA_KEY_R: { struct camu_view *view = get_view_from_mouse_pos(scr); if (view) { if (SCREEN_MOD2(scr)) { switch (view->rotation) { case CAMU_VIEW_ROTATION_0: case CAMU_VIEW_ROTATION_360: view->rotation = CAMU_VIEW_ROTATION_90; break; case CAMU_VIEW_ROTATION_270: view->rotation = CAMU_VIEW_ROTATION_0; break; case CAMU_VIEW_ROTATION_180: view->rotation = CAMU_VIEW_ROTATION_270; break; case CAMU_VIEW_ROTATION_90: view->rotation = CAMU_VIEW_ROTATION_180; break; } } else if (SCREEN_MOD1(scr)) { switch (view->rotation) { case CAMU_VIEW_ROTATION_0: case CAMU_VIEW_ROTATION_360: view->rotation = CAMU_VIEW_ROTATION_270; break; case CAMU_VIEW_ROTATION_270: view->rotation = CAMU_VIEW_ROTATION_180; break; case CAMU_VIEW_ROTATION_180: view->rotation = CAMU_VIEW_ROTATION_90; break; case CAMU_VIEW_ROTATION_90: view->rotation = CAMU_VIEW_ROTATION_0; break; } } else if (scr->vr_emulation) { view->fov = AL_TO_RADIANS(CAMU_DEFAULT_FOV); } else { view->mode = CAMU_DEFAULT_VIEW; } camu_view_calculate(view, scr->width, scr->height); return true; } break; } case STELA_KEY_T: { struct camu_view *view = get_view_from_mouse_pos(scr); if (view) { scr->window->resize(scr->window, ceil(view->width * view->zoom), ceil(view->height * view->zoom)); } break; } case STELA_KEY_EQUAL: { struct camu_view *view = get_view_from_mouse_pos(scr); if (view) { camu_view_zoom_simple(view, scr->width, scr->height, scr->width / 2.0, scr->height / 2.0, 0.05); return true; } break; } case STELA_KEY_MINUS: { struct camu_view *view = get_view_from_mouse_pos(scr); if (view) { camu_view_zoom_simple(view, scr->width, scr->height, scr->width / 2.0, scr->height / 2.0, -0.05); return true; } break; } case STELA_KEY_N: scr->scaling_disabled = !scr->scaling_disabled; if (scr->scaling_disabled) { scr->renderer->set(scr->renderer, CAMU_RENDERER_SCALING, CAMU_SCALING_NEAREST); log_info("Scaling filters disabled."); } else { scr->renderer->set(scr->renderer, CAMU_RENDERER_SCALING, CAMU_SCALING_DEFAULT); log_info("Scaling filters enabled."); } return true; case STELA_KEY_C: scr->transparent_background = !scr->transparent_background; if (scr->transparent_background) { scr->window->set_decorations(scr->window, false); scr->renderer->set(scr->renderer, CAMU_RENDERER_BACKGROUND, CAMU_BACKGROUND_TRANSPARENT); log_info("Background transparent."); } else { scr->window->set_decorations(scr->window, true); scr->renderer->set(scr->renderer, CAMU_RENDERER_BACKGROUND, CAMU_BACKGROUND_DEFAULT); log_info("Background solid."); } // Force refresh for if there's no video buffer. atomic_add(u32)(&scr->force_refresh, 1, AL_ATOMIC_RELAXED); return true; case STELA_KEY_V: { if (SCREEN_MOD1(scr)) { scr->vr_calibrate_x = scr->last_pointer_x / scr->width; scr->vr_calibrate_y = scr->last_pointer_y / scr->height; } else { struct camu_view *view = get_view_from_mouse_pos(scr); if (SCREEN_MOD2(scr) && scr->vr_emulation == CAMU_VR_180) { scr->vr_emulation = CAMU_VR_360; log_info("VR 360 degrees."); break; } if (!scr->vr_emulation) { if (view) view->mode = CAMU_VIEW_AUTOFIT; scr->flags = (scr->flags & ~CAMU_SCREEN_ZOOM_PAN_SIMPLE) | CAMU_SCREEN_ZOOM_FOV; scr->window->set_cursor(scr->window, STELA_CURSOR_HIDDEN); scr->vr_emulation = SCREEN_MOD2(scr) ? CAMU_VR_360 : CAMU_VR_180; log_info("VR %s degrees.", (scr->vr_emulation == CAMU_VR_180) ? "180" : "360"); } else { if (view) view->mode = CAMU_DEFAULT_VIEW; scr->flags = (scr->flags & ~CAMU_SCREEN_ZOOM_FOV) | CAMU_SCREEN_ZOOM_PAN_SIMPLE; scr->window->set_cursor(scr->window, STELA_CURSOR_NORMAL); scr->vr_emulation = CAMU_VR_DISABLED; log_info("VR disabled."); } if (view) { camu_view_calculate(view, scr->width, scr->height); } } break; } default: break; } break; case STELA_BUTTON_RELEASED: break; default: break; } return false; } static bool key_immediate_callback(void *userdata, u8 state, u16 button) { struct camu_screen *scr = (struct camu_screen *)userdata; switch (state) { case STELA_BUTTON_PRESSED: switch (button) { case STELA_KEY_F: scr->fullscreen = !scr->fullscreen; // On X11 and Wayland, MOUSE2 will be forcefully released when entering fullscreen. if (!SCREEN_MOUSE_MOD(scr)) { scr->window->set_cursor(scr->window, scr->fullscreen ? STELA_CURSOR_HIDDEN : STELA_CURSOR_NORMAL); } //#ifdef STELA_API_DX11 #if 0 // Stela's window_win32 fullscreen mode (borderless) is likely more desirable here. scr->renderer->set(scr->renderer, CAMU_RENDERER_FULLSCREEN, scr->fullscreen); #else scr->window->toggle_fullscreen(scr->window); #endif return true; case STELA_KEY_LEFT: if (scr->touch_mode) { s32 n = -1; scr->callback(scr->userdata, CAMU_SCREEN_SKIP, &n); return true; } break; case STELA_KEY_RIGHT: if (scr->touch_mode) { s32 n = 1; scr->callback(scr->userdata, CAMU_SCREEN_SKIP, &n); return true; } break; } break; default: break; } return false; } bool camu_screen_init(struct camu_screen *scr) { atomic_store(s32)(&scr->state, CAMU_SCREEN_PAUSED, AL_ATOMIC_RELAXED); scr->window = stl_window_create(); scr->window->render_callback = render_callback; scr->window->resize_callback = resize_callback; scr->window->pointer_pos_callback = pointer_pos_callback; scr->window->touch_callback = touch_callback; scr->window->scroll_callback = scroll_callback; scr->window->mouse_button_callback = mouse_button_callback; scr->window->key_callback = key_callback; scr->window->key_immediate_callback = key_immediate_callback; scr->window->should_close_callback = should_close_callback; scr->window->userdata = scr; scr->renderer = NULL; atomic_store(u32)(&scr->force_refresh, 1, AL_ATOMIC_RELAXED); scr->flags = CAMU_SCREEN_ZOOM_PAN_SIMPLE; scr->scaling_disabled = false; scr->transparent_background = false; #ifdef CAMU_HAVE_SUBTITLES scr->subtitles_enabled = true; #endif scr->last_click_ts = SCREEN_INVALID_TS; scr->last_pointer_x = 0.0; scr->last_pointer_y = 0.0; #ifdef CAMU_SCREEN_DRAG_SEEK scr->last_seek_ts = 0; #endif scr->touch_mode = false; scr->vr_emulation = CAMU_VR_DISABLED; scr->vr_left_eye = false; scr->vr_calibrate_x = 0.5; scr->vr_calibrate_y = 0.5; al_array_init(scr->videos); #ifdef CAMU_SCREEN_THREADED al_array_init(scr->add_queue); al_array_init(scr->rem_queue); atomic_store(bool)(&scr->queued, false, AL_ATOMIC_RELAXED); nn_mutex_init(&scr->mutex); #endif return true; } #ifdef STELA_EVENT_BUFFER static nn_thread_result NNWT_THREADCALL event_thread(void *userdata) { struct camu_screen *scr = (struct camu_screen *)userdata; nn_thread_set_name("window_events"); while (!scr->window->closed) { scr->window->poll(scr->window, true); scr->window->process_events(scr->window); } return 0; } #endif bool camu_screen_create_window(struct camu_screen *scr, const char *name) { u32 flags = STELA_WINDOW_VSYNC; if (!scr->window->create_window(scr->window, CAMU_SCREEN_WIDTH, CAMU_SCREEN_HEIGHT, flags, "", name, "camu_screen")) { return false; } scr->width = scr->window->width; scr->height = scr->window->height; scr->fullscreen = false; #ifdef STELA_EVENT_BUFFER nn_thread_create(&scr->thread, event_thread, scr); #endif return true; } bool camu_screen_create_renderer(struct camu_screen *scr, struct camu_renderer *renderer) { scr->renderer = renderer; return renderer->create_renderer(renderer, &scr->width, &scr->height, #if defined STELA_API_VULKAN scr->window->get_vk_proc_address, scr->window->vk_create_surface, scr->window->vk_destroy_surface, scr->window->vk_get_extensions, #elif defined STELA_API_DX11 scr->window->win32_window, #elif defined STELA_API_OPENGL #ifdef STELA_USE_EGL scr->window->egl_display, scr->window->egl_context, #endif scr->window->get_gl_proc_address, scr->window->gl_loader_load, scr->window->gl_make_current, scr->window->gl_release_current, scr->window->gl_swap_buffers, #endif scr->window); } static void add_buffer_internal(struct camu_screen *scr, struct camu_video_buffer *buf) { struct camu_screen_video *active; al_array_foreach_ptr(scr->videos, i, active) { al_assert(active->buf != buf); } struct camu_screen_video video = { 0 }; video.buf = buf; struct camu_video_format *fmt = &buf->fmt.req; if (buf->view.mode != CAMU_VIEW_NONE) { video.view = buf->view; } else { u8 rotation = buf->view.rotation; if (rotation != CAMU_VIEW_ROTATION_0) { log_info("Buffer has initial rotation of %s degrees.", camu_view_rotation_name(rotation)); } camu_view_init(&video.view, fmt->width, fmt->height, rotation); } camu_view_calculate(&video.view, scr->width, scr->height); al_array_push(scr->videos, video); } static void remove_buffer_internal(struct camu_screen *scr, struct camu_video_buffer *buf) { struct camu_screen_video *video; al_array_foreach_ptr(scr->videos, i, video) { if (video->buf == buf) { buf->view = video->view; al_array_remove_at(scr->videos, i); #ifdef CAMU_SCREEN_THREADED atomic_store(bool)(&buf->ref, false, AL_ATOMIC_RELAXED); #endif break; } } } #ifdef CAMU_SCREEN_THREADED static void run_queue_internal(struct camu_screen *scr) { struct camu_video_buffer *buf; al_array_foreach(scr->rem_queue, i, buf) { remove_buffer_internal(scr, buf); } scr->rem_queue.count = 0; // src->videos cannot be touched from outside of the render context. // For example, reserving space inside of screen_add_buffer() instead // of here would be very wrong. al_array_reserve(scr->videos, scr->videos.count + scr->add_queue.count); al_array_foreach(scr->add_queue, i, buf) { add_buffer_internal(scr, buf); } scr->add_queue.count = 0; } #endif void camu_screen_add_buffer(struct camu_screen *scr, struct camu_video_buffer *buf) { #ifdef CAMU_SCREEN_THREADED nn_mutex_lock(&scr->mutex); struct camu_video_buffer *queued; al_array_foreach(scr->add_queue, i, queued) { if (queued == buf) { nn_mutex_unlock(&scr->mutex); al_assert_and_return(); } } al_array_foreach_rev(scr->rem_queue, i, queued) { if (queued == buf) { al_array_remove_at(scr->rem_queue, i); bool queue_empty = scr->add_queue.count + scr->rem_queue.count == 0; if (queue_empty) { atomic_store(bool)(&scr->queued, false, AL_ATOMIC_RELAXED); } nn_mutex_unlock(&scr->mutex); return; } } al_array_push(scr->add_queue, buf); #ifdef CAMU_SCREEN_THREADED atomic_store(bool)(&buf->ref, true, AL_ATOMIC_RELAXED); #endif atomic_store(bool)(&scr->queued, true, AL_ATOMIC_RELAXED); nn_mutex_unlock(&scr->mutex); #else add_buffer_internal(scr, buf); #endif camu_screen_wake(scr); } void camu_screen_remove_buffer(struct camu_screen *scr, struct camu_video_buffer *buf) { #ifdef CAMU_SCREEN_THREADED nn_mutex_lock(&scr->mutex); struct camu_video_buffer *queued; al_array_foreach(scr->rem_queue, i, queued) { if (queued == buf) { // A sink entry that has an added video buffer (state set to BUFFER_ADDED) // that was previously removed (called screen_remove_buffer()), could // then be removed again without ever calling screen_add_buffer() // if that entry's audio buffer was not yet buffered. nn_mutex_unlock(&scr->mutex); return; } } al_array_foreach_rev(scr->add_queue, i, queued) { if (queued == buf) { al_array_remove_at(scr->add_queue, i); #ifdef CAMU_SCREEN_THREADED atomic_store(bool)(&buf->ref, false, AL_ATOMIC_RELAXED); #endif bool queue_empty = scr->add_queue.count + scr->rem_queue.count == 0; if (queue_empty) { atomic_store(bool)(&scr->queued, false, AL_ATOMIC_RELAXED); } nn_mutex_unlock(&scr->mutex); return; } } al_array_push(scr->rem_queue, buf); atomic_store(bool)(&scr->queued, true, AL_ATOMIC_RELAXED); nn_mutex_unlock(&scr->mutex); #else remove_buffer_internal(scr, buf); #endif camu_screen_wake(scr); } #ifdef CAMU_SCREEN_THREADED void camu_screen_run_queue(struct camu_screen *scr) { nn_mutex_lock(&scr->mutex); run_queue_internal(scr); atomic_store(bool)(&scr->queued, false, AL_ATOMIC_RELAXED); nn_mutex_unlock(&scr->mutex); } void camu_screen_clear(struct camu_screen *scr) { nn_mutex_lock(&scr->mutex); struct camu_screen_video *video; al_array_foreach_ptr(scr->videos, i, video) { struct camu_video_buffer *buf = video->buf; atomic_store(bool)(&buf->ref, false, AL_ATOMIC_RELAXED); } nn_mutex_unlock(&scr->mutex); } #endif void camu_screen_set_state(struct camu_screen *scr, s32 state) { // If state = PAUSED, remove_buffer() relies on screen_wake(). // Unlike mixer_pause() there are no special considerations needed for // the buffer queue at the point we set state to PAUSED. atomic_store(s32)(&scr->state, state, AL_ATOMIC_RELEASE); if (state == CAMU_SCREEN_PAUSED) { atomic_add(u32)(&scr->force_refresh, 1, AL_ATOMIC_RELAXED); } } void camu_screen_force_refresh(struct camu_screen *scr) { atomic_add(u32)(&scr->force_refresh, 1, AL_ATOMIC_RELAXED); camu_screen_wake(scr); } #define CAMU_SCREEN_POLL_HZ 432 bool camu_screen_poll(struct camu_screen *scr, bool block) { #ifdef STELA_EVENT_BUFFER if (block) { nn_thread_sleep(NNWT_TS_FROM_USEC(1000000 / CAMU_SCREEN_POLL_HZ)); } stl_window_read_events(scr->window); #else scr->window->poll(scr->window, block); scr->window->process_events(scr->window); #endif #ifdef STELA_PAUSE return scr->window->should_refresh(scr->window); #else return true; #endif } bool camu_screen_tick(struct camu_screen *scr, bool *force) { s32 state = atomic_load(s32)(&scr->state, AL_ATOMIC_RELAXED); u32 force_refresh = atomic_load(u32)(&scr->force_refresh, AL_ATOMIC_ACQUIRE); bool paused = !scr->vr_emulation && state == CAMU_SCREEN_PAUSED && force_refresh == 0; bool do_render = camu_screen_poll(scr, paused) || !paused; force_refresh = atomic_load(u32)(&scr->force_refresh, AL_ATOMIC_ACQUIRE); do_render |= force_refresh > 0; *force = force_refresh > 0; log_trace("tick (do_render: %s, force_refresh: %u).", BOOLSTR(do_render), force_refresh); if (force_refresh > 0) { atomic_sub(u32)(&scr->force_refresh, 1, AL_ATOMIC_RELEASE); } return do_render; } void camu_screen_wake(struct camu_screen *scr) { #if defined STELA_POLL_INLINE && defined STELA_PAUSE scr->window->wake(scr->window); #else (void)scr; #endif } void camu_screen_close(struct camu_screen *scr) { #ifdef CAMU_SCREEN_THREADED al_array_free(scr->add_queue); al_array_free(scr->rem_queue); nn_mutex_destroy(&scr->mutex); #endif al_array_free(scr->videos); #ifdef STELA_EVENT_BUFFER nn_thread_join(&scr->thread); #endif if (scr->window) { scr->window->free(&scr->window); } }