smoother fmv. better voices in game. minor other fixes

This commit is contained in:
izzy2lost
2026-03-19 01:01:26 -04:00
parent c833a7119e
commit 5681bf2fc9
11 changed files with 342 additions and 44 deletions
@@ -1,5 +1,8 @@
package com.izzy2lost.x1box
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.view.KeyEvent
import org.libsdl.app.SDLControllerManager
@@ -8,9 +11,16 @@ import org.libsdl.app.SDLControllerManager
*/
class ControllerInputBridge : OnScreenController.ControllerListener {
private data class ButtonDispatchState(
var isDown: Boolean = false,
var pressedAtMs: Long = 0L,
var pendingRelease: Runnable? = null,
)
companion object {
// Virtual device ID for on-screen controller
const val VIRTUAL_DEVICE_ID = -2
private const val MIN_TAP_HOLD_MS = 50L
// Axis indices for SDL
const val AXIS_LEFT_X = 0
@@ -21,33 +31,57 @@ class ControllerInputBridge : OnScreenController.ControllerListener {
const val AXIS_RIGHT_TRIGGER = 5
}
private val mainHandler = Handler(Looper.getMainLooper())
private val buttonStates = mutableMapOf<OnScreenController.Button, ButtonDispatchState>()
override fun onButtonPressed(button: OnScreenController.Button) {
val state = buttonStates.getOrPut(button) { ButtonDispatchState() }
state.pendingRelease?.let(mainHandler::removeCallbacks)
state.pendingRelease = null
state.pressedAtMs = SystemClock.uptimeMillis()
if (state.isDown) {
return
}
state.isDown = true
try {
when (button) {
OnScreenController.Button.LEFT_TRIGGER,
OnScreenController.Button.RIGHT_TRIGGER ->
setTriggerState(button, pressed = true)
else -> {
val keyCode = getKeyCodeForButton(button)
SDLControllerManager.onNativePadDown(VIRTUAL_DEVICE_ID, keyCode)
}
}
dispatchButtonState(button, pressed = true)
} catch (e: Exception) {
DebugLog.e("ControllerBridge", e) { "Error on button press: ${e.message}" }
}
}
override fun onButtonReleased(button: OnScreenController.Button) {
try {
when (button) {
OnScreenController.Button.LEFT_TRIGGER,
OnScreenController.Button.RIGHT_TRIGGER ->
setTriggerState(button, pressed = false)
else -> {
val keyCode = getKeyCodeForButton(button)
SDLControllerManager.onNativePadUp(VIRTUAL_DEVICE_ID, keyCode)
val state = buttonStates.getOrPut(button) { ButtonDispatchState() }
state.pendingRelease?.let(mainHandler::removeCallbacks)
state.pendingRelease = null
if (!state.isDown) {
return
}
val elapsed = SystemClock.uptimeMillis() - state.pressedAtMs
val remaining = MIN_TAP_HOLD_MS - elapsed
if (remaining > 0L) {
val releaseTask = Runnable {
state.pendingRelease = null
if (!state.isDown) {
return@Runnable
}
state.isDown = false
try {
dispatchButtonState(button, pressed = false)
} catch (e: Exception) {
DebugLog.e("ControllerBridge", e) { "Error on delayed button release: ${e.message}" }
}
}
state.pendingRelease = releaseTask
mainHandler.postDelayed(releaseTask, remaining)
return
}
state.isDown = false
try {
dispatchButtonState(button, pressed = false)
} catch (e: Exception) {
DebugLog.e("ControllerBridge", e) { "Error on button release: ${e.message}" }
}
@@ -94,6 +128,38 @@ class ControllerInputBridge : OnScreenController.ControllerListener {
}
}
fun reset() {
buttonStates.forEach { (button, state) ->
state.pendingRelease?.let(mainHandler::removeCallbacks)
state.pendingRelease = null
if (!state.isDown) {
return@forEach
}
state.isDown = false
try {
dispatchButtonState(button, pressed = false)
} catch (e: Exception) {
DebugLog.e("ControllerBridge", e) { "Error resetting $button: ${e.message}" }
}
}
}
private fun dispatchButtonState(button: OnScreenController.Button, pressed: Boolean) {
when (button) {
OnScreenController.Button.LEFT_TRIGGER,
OnScreenController.Button.RIGHT_TRIGGER ->
setTriggerState(button, pressed)
else -> {
val keyCode = getKeyCodeForButton(button)
if (pressed) {
SDLControllerManager.onNativePadDown(VIRTUAL_DEVICE_ID, keyCode)
} else {
SDLControllerManager.onNativePadUp(VIRTUAL_DEVICE_ID, keyCode)
}
}
}
}
private fun setTriggerState(button: OnScreenController.Button, pressed: Boolean) {
val keyCode = getKeyCodeForButton(button)
val axis = when (button) {
@@ -99,6 +99,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
// gesture panel, notification shade, or dialog appears). Without this,
// triggers can stay "pressed" if the touch UP event is never delivered.
onScreenController?.resetAllInputs()
controllerBridge?.reset()
}
}
@@ -218,6 +219,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
override fun onPause() {
swipeUpGestureRecognizer.reset()
onScreenController?.resetAllInputs()
controllerBridge?.reset()
super.onPause()
}
+36 -3
View File
@@ -264,6 +264,22 @@ static int getenv_int_clamped(const char *name, int min_value, int max_value,
return (int)parsed;
}
static void monitor_hold_last_sample(MCPXAPUState *s, uint8_t *stream, int len)
{
int frame_bytes = sizeof(s->monitor.last_output_sample);
int frames = len / frame_bytes;
int16_t *out = (int16_t *)stream;
for (int i = 0; i < frames; i++) {
out[i * 2 + 0] = s->monitor.last_output_sample[0];
out[i * 2 + 1] = s->monitor.last_output_sample[1];
}
int tail_bytes = len - (frames * frame_bytes);
if (tail_bytes > 0) {
memset(stream + (frames * frame_bytes), 0, tail_bytes);
}
}
static void monitor_sink_cb(void *opaque, uint8_t *stream, int free_b)
{
MCPXAPUState *s = MCPX_APU_DEVICE(opaque);
@@ -274,7 +290,12 @@ static void monitor_sink_cb(void *opaque, uint8_t *stream, int free_b)
}
int avail = 0;
for (int i = 0; i < 10; i++) {
#ifdef __ANDROID__
int wait_attempts = 24;
#else
int wait_attempts = 10;
#endif
for (int i = 0; i < wait_attempts; i++) {
qemu_spin_lock(&s->monitor.fifo_lock);
avail = fifo8_num_used(&s->monitor.fifo);
qemu_spin_unlock(&s->monitor.fifo_lock);
@@ -304,7 +325,14 @@ static void monitor_sink_cb(void *opaque, uint8_t *stream, int free_b)
}
if (copied < free_b) {
memset(stream + copied, 0, free_b - copied);
monitor_hold_last_sample(s, stream + copied, free_b - copied);
}
if (free_b >= sizeof(s->monitor.last_output_sample)) {
int16_t *last = (int16_t *)(stream + free_b -
sizeof(s->monitor.last_output_sample));
s->monitor.last_output_sample[0] = last[0];
s->monitor.last_output_sample[1] = last[1];
}
qemu_cond_broadcast(&s->cond);
@@ -317,11 +345,16 @@ static void monitor_init(MCPXAPUState *d)
d->monitor.device_buffer_bytes = 0;
d->monitor.queued_bytes_low = 0;
d->monitor.queued_bytes_high = 0;
d->monitor.last_output_sample[0] = 0;
d->monitor.last_output_sample[1] = 0;
int fifo_frames = 3;
int audio_samples = 512;
#ifdef __ANDROID__
fifo_frames = 16;
/* Give Android a little more audio headroom to ride out short stalls
* without increasing the device callback size again.
*/
fifo_frames = 24;
audio_samples = 2048;
fifo_frames = getenv_int_clamped("XEMU_ANDROID_AUDIO_FIFO_FRAMES", 3, 32,
fifo_frames);
+1
View File
@@ -102,6 +102,7 @@ typedef struct MCPXAPUState {
struct {
McpxApuDebugMonitorPoint point;
int16_t frame_buf[256][2]; // 1 EP frame (0x400 bytes), 8 buffered
int16_t last_output_sample[2];
QemuSpin fifo_lock;
Fifo8 fifo;
int fifo_capacity_bytes;
+52 -10
View File
@@ -1751,6 +1751,43 @@ voice_work_dispatch(MCPXAPUState *d,
qemu_mutex_lock(&vwd->lock);
if (vwd->queue_len) {
if (vwd->num_workers == 1) {
VoiceWorker *worker = &vwd->workers[0];
int voice_count = vwd->queue_len;
memset(worker->mixbins, 0, sizeof(worker->mixbins));
if (d->monitor.point == MCPX_APU_DEBUG_MON_VP) {
memset(worker->sample_buf, 0, sizeof(worker->sample_buf));
}
for (int i = 0; i < vwd->queue_len; i++) {
voice_process(d, worker->mixbins, worker->sample_buf,
vwd->queue[i].voice, vwd->queue[i].list);
}
voice_work_release_voice_locks(d);
vwd->queue_len = 0;
for (int b = 0; b < NUM_MIXBINS; b++) {
for (int s = 0; s < NUM_SAMPLES_PER_FRAME; s++) {
mixbins[b][s] += worker->mixbins[b][s];
}
}
if (d->monitor.point == MCPX_APU_DEBUG_MON_VP) {
for (int i = 0; i < NUM_SAMPLES_PER_FRAME; i++) {
d->vp.sample_buf[i][0] += worker->sample_buf[i][0];
d->vp.sample_buf[i][1] += worker->sample_buf[i][1];
}
}
g_dbg.vp.workers[0].num_voices = voice_count;
g_dbg.vp.workers[0].time_us =
qemu_clock_get_us(QEMU_CLOCK_REALTIME) - start_time;
g_dbg.vp.total_worker_time_us = g_dbg.vp.workers[0].time_us;
qemu_mutex_unlock(&vwd->lock);
return;
}
memset(vwd->mixbins, 0, sizeof(vwd->mixbins));
// Signal workers and wait for completion
@@ -1782,8 +1819,9 @@ static int mcpx_apu_default_vp_worker_count(void)
#ifdef __ANDROID__
/*
* Mobile SoCs are often oversubscribed already (TCG + render + I/O).
* Keep VP worker defaults conservative to reduce thread contention and
* audio underruns; allow explicit override via env.
* Keep VP worker defaults very conservative to reduce thread contention and
* leave more CPU time for the emulation threads that gate frametime.
* Allow explicit override via env.
*/
const char *value = getenv("XEMU_ANDROID_VP_WORKERS");
if (value && value[0] != '\0') {
@@ -1794,16 +1832,10 @@ static int mcpx_apu_default_vp_worker_count(void)
}
}
if (cpu_count <= 2) {
if (cpu_count <= 6) {
return 1;
}
if (cpu_count <= 4) {
return 2;
}
if (cpu_count <= 6) {
return 3;
}
return 4;
return 2;
#else
return cpu_count;
#endif
@@ -1829,6 +1861,10 @@ static void voice_work_init(MCPXAPUState *d)
qemu_mutex_lock(&vwd->lock);
qemu_cond_init(&vwd->work_pending);
qemu_cond_init(&vwd->work_finished);
if (vwd->num_workers == 1) {
qemu_mutex_unlock(&vwd->lock);
return;
}
for (int i = 0; i < vwd->num_workers; i++) {
vwd->workers_pending |= 1 << i;
qemu_thread_create(&vwd->workers[i].thread, "mcpx.voice_worker",
@@ -1843,6 +1879,12 @@ static void voice_work_finalize(MCPXAPUState *d)
{
VoiceWorkDispatch *vwd = &d->vp.voice_work_dispatch;
if (vwd->num_workers == 1) {
g_free(vwd->workers);
vwd->workers = NULL;
return;
}
qemu_mutex_lock(&vwd->lock);
vwd->workers_should_exit = true;
qemu_cond_broadcast(&vwd->work_pending);
+1 -1
View File
@@ -180,7 +180,7 @@ static void pgraph_gl_finalize(NV2AState *d)
static void pgraph_gl_flip_stall(NV2AState *d)
{
NV2A_GL_DFRAME_TERMINATOR();
glFinish();
glFlush();
}
static void pgraph_gl_flush(NV2AState *d)
+5
View File
@@ -193,6 +193,11 @@ typedef struct PGRAPHGLState {
bool download_dirty_surfaces_pending;
QemuEvent dirty_surfaces_download_complete; // common
#ifdef __ANDROID__
GLuint gl_download_pbo;
size_t gl_download_pbo_size;
#endif
TextureBinding *texture_binding[NV2A_MAX_TEXTURES];
Lru texture_cache;
TextureLruNode *texture_cache_entries;
+75 -9
View File
@@ -92,6 +92,54 @@ static bool android_drain_gl_errors_silent(void)
return had_error;
}
static void android_glo_readpixels(PGRAPHGLState *r, GLenum gl_format,
GLenum gl_type,
unsigned int bytes_per_pixel,
unsigned int stride,
unsigned int width,
unsigned int height, bool vflip,
void *data)
{
size_t required_size = stride * height;
glBindBuffer(GL_PIXEL_PACK_BUFFER, r->gl_download_pbo);
if (required_size > r->gl_download_pbo_size) {
glBufferData(GL_PIXEL_PACK_BUFFER, required_size, NULL,
GL_STREAM_READ);
r->gl_download_pbo_size = required_size;
}
int rl, pa;
glGetIntegerv(GL_PACK_ROW_LENGTH, &rl);
glGetIntegerv(GL_PACK_ALIGNMENT, &pa);
glPixelStorei(GL_PACK_ROW_LENGTH, stride / bytes_per_pixel);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, width, height, gl_format, gl_type, 0);
void *mapped_data = glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0,
required_size, GL_MAP_READ_BIT);
if (mapped_data) {
if (vflip) {
GLubyte *b = (GLubyte *) data;
GLubyte *c =
&((GLubyte *) mapped_data)[stride * (height - 1)];
for (unsigned int irow = 0; irow < height; irow++) {
memcpy(b, c, width * bytes_per_pixel);
b += stride;
c -= stride;
}
} else {
memcpy(data, mapped_data, required_size);
}
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
}
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
glPixelStorei(GL_PACK_ROW_LENGTH, rl);
glPixelStorei(GL_PACK_ALIGNMENT, pa);
}
static void android_sanitize_surface_format(PGRAPHGLState *r,
SurfaceFormatInfo *fmt)
{
@@ -1375,8 +1423,8 @@ static bool android_surface_download_depth16_to_guest(NV2AState *d,
}
rgba_pixels = g_malloc(read_height * rgba_stride);
glo_readpixels(GL_RGBA, GL_UNSIGNED_BYTE, 4, rgba_stride, read_width,
read_height, flip, rgba_pixels);
android_glo_readpixels(r, GL_RGBA, GL_UNSIGNED_BYTE, 4, rgba_stride,
read_width, read_height, flip, rgba_pixels);
if (android_log_surface_download_errors(
"surface_download_depth16: pack read", surface)) {
@@ -1533,8 +1581,9 @@ static bool android_surface_download_z24s8_to_guest(NV2AState *d,
}
depth_pixels = g_malloc(read_width * read_height * 4);
glo_readpixels(GL_RGBA, GL_UNSIGNED_BYTE, 4, read_width * 4, read_width,
read_height, flip, depth_pixels);
android_glo_readpixels(r, GL_RGBA, GL_UNSIGNED_BYTE, 4,
read_width * 4, read_width, read_height, flip,
depth_pixels);
if (android_log_surface_download_errors(
"surface_download_z24s8: post-read-depth-pack", surface)) {
goto cleanup;
@@ -1553,9 +1602,9 @@ static bool android_surface_download_z24s8_to_guest(NV2AState *d,
}
stencil_pixels = g_malloc0(read_width * read_height);
glo_readpixels(GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, 1, read_width,
read_width, read_height, flip,
stencil_pixels);
android_glo_readpixels(r, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, 1,
read_width, read_width, read_height, flip,
stencil_pixels);
if (android_drain_gl_errors_silent()) {
if (!warned_stencil_readback_unsupported) {
warned_stencil_readback_unsupported = true;
@@ -1713,8 +1762,9 @@ static void surface_download_to_buffer(NV2AState *d, SurfaceBinding *surface,
uint8_t *rgba_linear = rgba_pixels;
uint8_t *linear_guest = pixels;
glo_readpixels(GL_RGBA, GL_UNSIGNED_BYTE, 4, rgba_stride,
read_width, read_height, flip, rgba_pixels);
android_glo_readpixels(pg->gl_renderer_state, GL_RGBA,
GL_UNSIGNED_BYTE, 4, rgba_stride,
read_width, read_height, flip, rgba_pixels);
android_log_surface_download_errors(
"surface_download_to_buffer: post-read_rgba8", surface);
@@ -1768,7 +1818,11 @@ static void surface_download_to_buffer(NV2AState *d, SurfaceBinding *surface,
gl_read_buf = pg->scale_buf;
}
#ifdef __ANDROID__
android_glo_readpixels(pg->gl_renderer_state,
#else
glo_readpixels(
#endif
surface->fmt.gl_format, surface->fmt.gl_type, surface->fmt.bytes_per_pixel,
pg->surface_scale_factor * surface->pitch,
pg->surface_scale_factor * surface->width,
@@ -2520,6 +2574,10 @@ void pgraph_gl_init_surfaces(PGRAPHState *pg)
pgraph_gl_reload_surface_scale_factor(pg);
glGenFramebuffers(1, &r->gl_framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, r->gl_framebuffer);
#ifdef __ANDROID__
glGenBuffers(1, &r->gl_download_pbo);
r->gl_download_pbo_size = 0;
#endif
QTAILQ_INIT(&r->surfaces);
r->downloads_pending = false;
qemu_event_init(&r->downloads_complete, false);
@@ -2558,6 +2616,14 @@ void pgraph_gl_finalize_surfaces(PGRAPHState *pg)
glDeleteFramebuffers(1, &r->gl_framebuffer);
r->gl_framebuffer = 0;
#ifdef __ANDROID__
if (r->gl_download_pbo) {
glDeleteBuffers(1, &r->gl_download_pbo);
r->gl_download_pbo = 0;
r->gl_download_pbo_size = 0;
}
#endif
finalize_render_to_texture(pg);
}
+74
View File
@@ -48,6 +48,7 @@
#define XEMU_INPUT_MIN_INPUT_UPDATE_INTERVAL_US 2500
#define XEMU_INPUT_MIN_RUMBLE_UPDATE_INTERVAL_US 2500
#define XEMU_INPUT_MIN_BUTTON_HOLD_US 50000
#if 0
static void xemu_input_print_controller_state(ControllerState *state)
@@ -92,6 +93,58 @@ const char *bound_drivers[4] = { DRIVER_DUKE, DRIVER_DUKE, DRIVER_DUKE,
DRIVER_DUKE };
int test_mode;
static ControllerState *xemu_input_find_sdl_controller(SDL_JoystickID id)
{
ControllerState *iter;
QTAILQ_FOREACH(iter, &available_controllers, entry) {
if (iter->type == INPUT_DEVICE_SDL_GAMECONTROLLER &&
iter->sdl_joystick_id == id) {
return iter;
}
}
return NULL;
}
static int xemu_input_sdl_button_to_button_id(uint8_t button)
{
switch (button) {
case SDL_CONTROLLER_BUTTON_A:
return 0;
case SDL_CONTROLLER_BUTTON_B:
return 1;
case SDL_CONTROLLER_BUTTON_X:
return 2;
case SDL_CONTROLLER_BUTTON_Y:
return 3;
case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
return 4;
case SDL_CONTROLLER_BUTTON_DPAD_UP:
return 5;
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
return 6;
case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
return 7;
case SDL_CONTROLLER_BUTTON_BACK:
return 8;
case SDL_CONTROLLER_BUTTON_START:
return 9;
case SDL_CONTROLLER_BUTTON_LEFTSHOULDER:
return 10;
case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER:
return 11;
case SDL_CONTROLLER_BUTTON_LEFTSTICK:
return 12;
case SDL_CONTROLLER_BUTTON_RIGHTSTICK:
return 13;
case SDL_CONTROLLER_BUTTON_GUIDE:
return 14;
default:
return -1;
}
}
static const char **port_index_to_settings_key_map[] = {
&g_config.input.bindings.port1,
&g_config.input.bindings.port2,
@@ -444,6 +497,17 @@ void xemu_input_process_sdl_events(const SDL_Event *event)
xemu_queue_notification(buf);
xemu_input_rebind_xmu(port);
}
} else if (event->type == SDL_CONTROLLERBUTTONDOWN) {
ControllerState *state =
xemu_input_find_sdl_controller(event->cbutton.which);
int button_id =
xemu_input_sdl_button_to_button_id(event->cbutton.button);
if (state && button_id >= 0) {
state->button_hold_until_us[button_id] =
qemu_clock_get_us(QEMU_CLOCK_REALTIME) +
XEMU_INPUT_MIN_BUTTON_HOLD_US;
}
} else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
DPRINTF("Controller Removed: %d\n", event->cdevice.which);
int handled = 0;
@@ -575,6 +639,8 @@ void xemu_input_update_sdl_kbd_controller_state(ControllerState *state)
void xemu_input_update_sdl_controller_state(ControllerState *state)
{
int64_t now = qemu_clock_get_us(QEMU_CLOCK_REALTIME);
state->buttons = 0;
memset(state->axis, 0, sizeof(state->axis));
if (!state->controller_map) {
@@ -641,6 +707,14 @@ void xemu_input_update_sdl_controller_state(ControllerState *state)
#undef INVERT_AXIS
for (int i = 0; i < 15; i++) {
if (state->button_hold_until_us[i] > now) {
state->buttons |= CONTROLLER_STATE_BUTTON_ID_TO_MASK(i);
} else {
state->button_hold_until_us[i] = 0;
}
}
// xemu_input_print_controller_state(state);
}
+1
View File
@@ -89,6 +89,7 @@ typedef struct ControllerState {
// Input state
uint16_t buttons;
int16_t axis[CONTROLLER_AXIS__COUNT];
int64_t button_hold_until_us[15];
// Rendering state hacked on here for convenience but needs to be moved (FIXME)
uint32_t animate_guide_button_end;
+12 -4
View File
@@ -1381,7 +1381,7 @@ void xemu_android_display_loop(void)
}
#ifdef __ANDROID__
xemu_android_refresh_frame_limit_from_env();
SDL_GL_SetSwapInterval(g_config.display.window.vsync ? 1 : 0);
SDL_GL_SetSwapInterval(0);
xemu_hud_init(m_window, m_context);
#endif
tcg_register_init_ctx();
@@ -1737,10 +1737,14 @@ void sdl2_gl_refresh(DisplayChangeListener *dcl)
*/
static int64_t last_update = 0;
#ifdef __ANDROID__
int64_t deadline = last_update + g_android_frame_interval_ns;
const int64_t frame_interval = g_android_frame_interval_ns;
#else
int64_t deadline = last_update + 16666666;
const int64_t frame_interval = 16666666;
#endif
if (last_update == 0) {
last_update = qemu_clock_get_ns(QEMU_CLOCK_REALTIME);
}
int64_t deadline = last_update + frame_interval;
#ifdef DEBUG_XEMU_C
int64_t sleep_acc = 0;
@@ -1775,7 +1779,11 @@ void sdl2_gl_refresh(DisplayChangeListener *dcl)
}
} else {
DPRINTF("zzZz %g %ld\n", (double)sleep_acc/1000000.0, spin_acc);
last_update = now;
if (now - deadline > frame_interval) {
last_update = now;
} else {
last_update = deadline;
}
break;
}
}