diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index fd318e4921..0b4bde0051 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -42,12 +42,13 @@ android { arguments += listOf( "-DXEMU_ANDROID_BUILD_ID=3", "-DXEMU_ENABLE_XISO_CONVERTER=ON", - "-DCMAKE_C_FLAGS_DEBUG=-O2 -g0", - "-DCMAKE_CXX_FLAGS_DEBUG=-O2 -g0", - "-DCMAKE_C_FLAGS_RELWITHDEBINFO=-O3 -g0", - "-DCMAKE_CXX_FLAGS_RELWITHDEBINFO=-O3 -g0", - "-DCMAKE_C_FLAGS_RELEASE=-O3 -g0", - "-DCMAKE_CXX_FLAGS_RELEASE=-O3 -g0" + "-DCMAKE_C_FLAGS_DEBUG=-O2 -g0 -march=armv8-a+simd -fvisibility=hidden -ffunction-sections -fdata-sections", + "-DCMAKE_CXX_FLAGS_DEBUG=-O2 -g0 -march=armv8-a+simd -fvisibility=hidden -ffunction-sections -fdata-sections", + "-DCMAKE_C_FLAGS_RELWITHDEBINFO=-O3 -g0 -march=armv8-a+simd -fvisibility=hidden -ffunction-sections -fdata-sections", + "-DCMAKE_CXX_FLAGS_RELWITHDEBINFO=-O3 -g0 -march=armv8-a+simd -fvisibility=hidden -ffunction-sections -fdata-sections", + "-DCMAKE_C_FLAGS_RELEASE=-O3 -g0 -march=armv8-a+simd -fvisibility=hidden -ffunction-sections -fdata-sections", + "-DCMAKE_CXX_FLAGS_RELEASE=-O3 -g0 -march=armv8-a+simd -fvisibility=hidden -ffunction-sections -fdata-sections", + "-DCMAKE_SHARED_LINKER_FLAGS=-Wl,--gc-sections" ) cppFlags += listOf("-std=c++17", "-fexceptions", "-frtti") } diff --git a/android/app/src/main/cpp/xemu_android.cpp b/android/app/src/main/cpp/xemu_android.cpp index bd95fd2c16..73389a28fa 100644 --- a/android/app/src/main/cpp/xemu_android.cpp +++ b/android/app/src/main/cpp/xemu_android.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -144,18 +145,29 @@ static void AppendNativeDebugLog(const char* level, const char* message) { SDL_LockMutex(g_native_debug_log_mutex); + /* Use POSIX write() with O_APPEND instead of std::ofstream to avoid + * repeated file open/close overhead and C++ stream construction. */ struct stat st {}; - const bool should_truncate = - stat(g_native_debug_log_path.c_str(), &st) == 0 && - st.st_size > kMaxDebugLogBytes; + if (stat(g_native_debug_log_path.c_str(), &st) == 0 && + st.st_size > kMaxDebugLogBytes) { + /* Truncate oversized log file */ + int tfd = open(g_native_debug_log_path.c_str(), O_WRONLY | O_TRUNC); + if (tfd >= 0) close(tfd); + } - std::ofstream out( - g_native_debug_log_path, - should_truncate ? (std::ios::out | std::ios::trunc) - : (std::ios::out | std::ios::app)); - if (out.is_open()) { - out << CurrentNativeLogTimestamp() << ' ' << level << '/' << kLogTag - << ": " << message << '\n'; + int fd = open(g_native_debug_log_path.c_str(), + O_WRONLY | O_CREAT | O_APPEND, 0644); + if (fd >= 0) { + char buf[2048]; + std::string ts = CurrentNativeLogTimestamp(); + int len = snprintf(buf, sizeof(buf), "%s %s/%s: %s\n", + ts.c_str(), level, kLogTag, message); + if (len > 0) { + size_t bytes_to_write = + (size_t)((len < (int)sizeof(buf) - 1) ? len : (int)sizeof(buf) - 1); + (void)write(fd, buf, bytes_to_write); + } + close(fd); } SDL_UnlockMutex(g_native_debug_log_mutex); diff --git a/android/app/src/main/cpp/xemu_fatx_import.c b/android/app/src/main/cpp/xemu_fatx_import.c index 185efed586..709b0fbb69 100644 --- a/android/app/src/main/cpp/xemu_fatx_import.c +++ b/android/app/src/main/cpp/xemu_fatx_import.c @@ -59,6 +59,7 @@ typedef struct XemuFatxCache { uint32_t entry_size; bool dirty; void *data; + size_t data_capacity; } XemuFatxCache; typedef struct XemuFatxFs { @@ -375,11 +376,15 @@ static bool xemu_fatx_populate_fat_cache(XemuFatxFs *fs, uint32_t index) cache->dirty = false; } - g_free(cache->data); cache->position = index; cache->entries = MIN(XEMU_FATX_FAT_CACHE_NUM_ENTRIES, fs->fat_entry_count - index); cache->entry_size = fs->fat_type == XEMU_FATX_FAT_TYPE_16 ? 2 : 4; - cache->data = g_malloc(cache->entries * cache->entry_size); + size_t needed = (size_t)cache->entries * cache->entry_size; + if (needed > cache->data_capacity) { + g_free(cache->data); + cache->data = g_malloc(needed); + cache->data_capacity = needed; + } xemu_fatx_dev_seek(fs, fs->fat_offset + (uint64_t)cache->position * cache->entry_size); diff --git a/hw/xbox/nv2a/pgraph/gl/renderer.h b/hw/xbox/nv2a/pgraph/gl/renderer.h index a59606fb87..9f066b6852 100644 --- a/hw/xbox/nv2a/pgraph/gl/renderer.h +++ b/hw/xbox/nv2a/pgraph/gl/renderer.h @@ -203,6 +203,8 @@ typedef struct PGRAPHGLState { size_t android_conv_buf_size; uint8_t *android_s2t_conv_buf; /* render-to-texture conversion */ size_t android_s2t_conv_buf_size; + uint8_t *android_tex_conv_buf; /* texture upload conversion */ + size_t android_tex_conv_buf_size; #endif TextureBinding *texture_binding[NV2A_MAX_TEXTURES]; diff --git a/hw/xbox/nv2a/pgraph/gl/texture.c b/hw/xbox/nv2a/pgraph/gl/texture.c index d8ec4638e8..a48679282b 100644 --- a/hw/xbox/nv2a/pgraph/gl/texture.c +++ b/hw/xbox/nv2a/pgraph/gl/texture.c @@ -29,6 +29,9 @@ #ifdef __ANDROID__ #include +#ifdef __aarch64__ +#include +#endif static void android_log_gl_errors(const char *ctx) { @@ -231,48 +234,116 @@ static void android_texture_convert_to_rgba8(const TextureShape s, } case NV097_SET_TEXTURE_FORMAT_COLOR_SZ_A8R8G8B8: case NV097_SET_TEXTURE_FORMAT_COLOR_SZ_I8_A8R8G8B8: - case NV097_SET_TEXTURE_FORMAT_COLOR_LU_IMAGE_A8R8G8B8: { - const uint8_t *pixel = src_row + x * 4; - out[0] = pixel[2]; - out[1] = pixel[1]; - out[2] = pixel[0]; - out[3] = pixel[3]; - break; - } + case NV097_SET_TEXTURE_FORMAT_COLOR_LU_IMAGE_A8R8G8B8: case NV097_SET_TEXTURE_FORMAT_COLOR_SZ_X8R8G8B8: case NV097_SET_TEXTURE_FORMAT_COLOR_LU_IMAGE_X8R8G8B8: { + bool preserve_alpha = + (s.color_format != NV097_SET_TEXTURE_FORMAT_COLOR_SZ_X8R8G8B8 && + s.color_format != NV097_SET_TEXTURE_FORMAT_COLOR_LU_IMAGE_X8R8G8B8); +#ifdef __aarch64__ + /* NEON BGRA→RGBA shuffle: process 4 pixels (16 bytes) at a time */ + static const uint8_t perm_arr[16] = + {2,1,0,3, 6,5,4,7, 10,9,8,11, 14,13,12,15}; + uint8x16_t vperm = vld1q_u8(perm_arr); + static const uint8_t alpha_mask_arr[16] = + {0,0,0,0xFF, 0,0,0,0xFF, 0,0,0,0xFF, 0,0,0,0xFF}; + uint8x16_t valpha_mask = preserve_alpha + ? vdupq_n_u8(0) + : vld1q_u8(alpha_mask_arr); + unsigned int remaining = width - x; + const uint8_t *sp = src_row + x * 4; + uint8_t *dp = out; + while (remaining >= 4) { + uint8x16_t v = vqtbl1q_u8(vld1q_u8(sp), vperm); + vst1q_u8(dp, vorrq_u8(v, valpha_mask)); + sp += 16; dp += 16; remaining -= 4; + } + while (remaining-- > 0) { + dp[0] = sp[2]; dp[1] = sp[1]; + dp[2] = sp[0]; dp[3] = preserve_alpha ? sp[3] : 0xFF; + sp += 4; dp += 4; + } + x = width; /* skip remaining scalar iterations */ +#else const uint8_t *pixel = src_row + x * 4; out[0] = pixel[2]; out[1] = pixel[1]; out[2] = pixel[0]; - out[3] = 0xFF; + out[3] = preserve_alpha ? pixel[3] : 0xFF; +#endif break; } case NV097_SET_TEXTURE_FORMAT_COLOR_SZ_B8G8R8A8: case NV097_SET_TEXTURE_FORMAT_COLOR_LU_IMAGE_B8G8R8A8: { +#ifdef __aarch64__ + /* NEON B8G8R8A8→RGBA8: rotate bytes within each pixel */ + static const uint8_t perm_arr[16] = + {1,2,3,0, 5,6,7,4, 9,10,11,8, 13,14,15,12}; + uint8x16_t vperm = vld1q_u8(perm_arr); + unsigned int remaining = width - x; + const uint8_t *sp = src_row + x * 4; + uint8_t *dp = out; + while (remaining >= 4) { + vst1q_u8(dp, vqtbl1q_u8(vld1q_u8(sp), vperm)); + sp += 16; dp += 16; remaining -= 4; + } + while (remaining-- > 0) { + dp[0] = sp[1]; dp[1] = sp[2]; + dp[2] = sp[3]; dp[3] = sp[0]; + sp += 4; dp += 4; + } + x = width; +#else const uint8_t *pixel = src_row + x * 4; out[0] = pixel[1]; out[1] = pixel[2]; out[2] = pixel[3]; out[3] = pixel[0]; +#endif break; } case NV097_SET_TEXTURE_FORMAT_COLOR_SZ_A8B8G8R8: case NV097_SET_TEXTURE_FORMAT_COLOR_LU_IMAGE_A8B8G8R8: { +#ifdef __aarch64__ + /* A8B8G8R8 is already RGBA8 — bulk memcpy */ + memcpy(out, src_row + x * 4, (width - x) * 4); + x = width; +#else const uint8_t *pixel = src_row + x * 4; out[0] = pixel[0]; out[1] = pixel[1]; out[2] = pixel[2]; out[3] = pixel[3]; +#endif break; } case NV097_SET_TEXTURE_FORMAT_COLOR_SZ_R8G8B8A8: case NV097_SET_TEXTURE_FORMAT_COLOR_LU_IMAGE_R8G8B8A8: { +#ifdef __aarch64__ + /* NEON R8G8B8A8→RGBA8: full byte reverse per pixel */ + static const uint8_t perm_arr[16] = + {3,2,1,0, 7,6,5,4, 11,10,9,8, 15,14,13,12}; + uint8x16_t vperm = vld1q_u8(perm_arr); + unsigned int remaining = width - x; + const uint8_t *sp = src_row + x * 4; + uint8_t *dp = out; + while (remaining >= 4) { + vst1q_u8(dp, vqtbl1q_u8(vld1q_u8(sp), vperm)); + sp += 16; dp += 16; remaining -= 4; + } + while (remaining-- > 0) { + dp[0] = sp[3]; dp[1] = sp[2]; + dp[2] = sp[1]; dp[3] = sp[0]; + sp += 4; dp += 4; + } + x = width; +#else const uint8_t *pixel = src_row + x * 4; out[0] = pixel[3]; out[1] = pixel[2]; out[2] = pixel[1]; out[3] = pixel[0]; +#endif break; } case NV097_SET_TEXTURE_FORMAT_COLOR_LU_IMAGE_G8B8: @@ -300,7 +371,8 @@ static void android_texture_convert_to_rgba8(const TextureShape s, } } -static void android_prepare_tex_upload(const TextureShape s, +static void android_prepare_tex_upload(PGRAPHGLState *r, + const TextureShape s, const uint8_t *src, unsigned int width, unsigned int height, @@ -310,11 +382,9 @@ static void android_prepare_tex_upload(const TextureShape s, GLint *ifmt, GLenum *fmt, GLenum *type, - const uint8_t **upload_data, - uint8_t **upload_tmp) + const uint8_t **upload_data) { *upload_data = src; - *upload_tmp = NULL; *ifmt = kelvin_color_format_gl_map[s.color_format].gl_internal_format; *fmt = kelvin_color_format_gl_map[s.color_format].gl_format; *type = kelvin_color_format_gl_map[s.color_format].gl_type; @@ -323,10 +393,16 @@ static void android_prepare_tex_upload(const TextureShape s, return; } - *upload_tmp = g_malloc(width * height * depth * 4); + size_t needed = (size_t)width * height * depth * 4; + if (needed > r->android_tex_conv_buf_size) { + g_free(r->android_tex_conv_buf); + r->android_tex_conv_buf = g_malloc(needed); + r->android_tex_conv_buf_size = needed; + } android_texture_convert_to_rgba8(s, src, width, height, depth, - row_pitch, slice_pitch, *upload_tmp); - *upload_data = *upload_tmp; + row_pitch, slice_pitch, + r->android_tex_conv_buf); + *upload_data = r->android_tex_conv_buf; *ifmt = GL_RGBA8; *fmt = GL_RGBA; *type = GL_UNSIGNED_BYTE; @@ -383,7 +459,7 @@ static void pgraph_gl_log_invalid_texture_range(NV2AState *d, #endif } -static TextureBinding* generate_texture(const TextureShape s, const uint8_t *texture_data, const uint8_t *palette_data); +static TextureBinding* generate_texture(PGRAPHGLState *r, const TextureShape s, const uint8_t *texture_data, const uint8_t *palette_data); static void texture_binding_destroy(gpointer data); struct pgraph_texture_possibly_dirty_struct { @@ -810,7 +886,7 @@ void pgraph_gl_bind_textures(NV2AState *d) if (key_out->binding == NULL) { // Must create the texture - key_out->binding = generate_texture(state, texture_data, palette_data); + key_out->binding = generate_texture(r, state, texture_data, palette_data); key_out->binding->data_hash = tex_data_hash; key_out->binding->scale = 1; #ifdef __ANDROID__ @@ -894,11 +970,13 @@ gl_internal_format_to_s3tc_enum(GLint gl_internal_format) } } -static void upload_gl_texture(GLenum gl_target, +static void upload_gl_texture(PGRAPHGLState *r, + GLenum gl_target, const TextureShape s, const uint8_t *texture_data, const uint8_t *palette_data) { + (void)r; /* used only in Android-specific paths */ ColorFormatInfo f = kelvin_color_format_gl_map[s.color_format]; nv2a_profile_inc_counter(NV2A_PROF_TEX_UPLOAD); #ifdef __ANDROID__ @@ -935,7 +1013,6 @@ static void upload_gl_texture(GLenum gl_target, { const uint8_t *pixel_data = converted ? converted : texture_data; const uint8_t *upload_data = pixel_data; - uint8_t *upload_tmp = NULL; unsigned int source_bpp = #ifdef __ANDROID__ android_texture_source_bpp(s, f.bytes_per_pixel, @@ -949,11 +1026,11 @@ static void upload_gl_texture(GLenum gl_target, GLint tex_ifmt = f.gl_internal_format; GLenum tex_type = f.gl_type; #ifdef __ANDROID__ - android_prepare_tex_upload(s, pixel_data, adjusted_width, + android_prepare_tex_upload(r, s, pixel_data, adjusted_width, adjusted_height, 1, row_pitch, row_pitch * adjusted_height, &tex_ifmt, &tex_fmt, &tex_type, - &upload_data, &upload_tmp); + &upload_data); #endif glPixelStorei(GL_UNPACK_ROW_LENGTH, upload_data == texture_data ? @@ -963,9 +1040,6 @@ static void upload_gl_texture(GLenum gl_target, tex_fmt, tex_type, upload_data); glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); -#ifdef __ANDROID__ - g_free(upload_tmp); -#endif } if (converted) { @@ -1000,8 +1074,10 @@ static void upload_gl_texture(GLenum gl_target, texture_data, width, height); unsigned int tex_width = width; unsigned int tex_height = height; + bool need_cubemap_border_strip = + s.cubemap && adjusted_width != s.width; - if (s.cubemap && adjusted_width != s.width) { + if (need_cubemap_border_strip) { // FIXME: Consider preserving the border. // There does not seem to be a way to reference the border // texels in a cubemap, so they are discarded. @@ -1019,7 +1095,7 @@ static void upload_gl_texture(GLenum gl_target, glTexImage2D(gl_target, level, GL_RGBA8, tex_width, tex_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, converted); g_free(converted); - if (s.cubemap && adjusted_width != s.width) { + if (need_cubemap_border_strip) { glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); if (physical_width == width) { @@ -1059,16 +1135,15 @@ static void upload_gl_texture(GLenum gl_target, { const uint8_t *upload_data = pixel_data; - uint8_t *upload_tmp = NULL; GLenum tex_fmt = f.gl_format; GLint tex_ifmt = f.gl_internal_format; GLenum tex_type = f.gl_type; #ifdef __ANDROID__ - android_prepare_tex_upload(s, pixel_data, tex_width, + android_prepare_tex_upload(r, s, pixel_data, tex_width, tex_height, 1, row_pitch, row_pitch * tex_height, &tex_ifmt, &tex_fmt, &tex_type, - &upload_data, &upload_tmp); + &upload_data); #endif glPixelStorei(GL_UNPACK_ROW_LENGTH, upload_data == pixel_data && @@ -1078,9 +1153,6 @@ static void upload_gl_texture(GLenum gl_target, tex_height, 0, tex_fmt, tex_type, upload_data); glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); -#ifdef __ANDROID__ - g_free(upload_tmp); -#endif } if (converted) { g_free(converted); @@ -1153,7 +1225,6 @@ static void upload_gl_texture(GLenum gl_target, { const uint8_t *pixel_data = converted ? converted : unswizzled; const uint8_t *upload_data = pixel_data; - uint8_t *upload_tmp = NULL; unsigned int source_bpp = #ifdef __ANDROID__ android_texture_source_bpp(s, f.bytes_per_pixel, @@ -1167,19 +1238,16 @@ static void upload_gl_texture(GLenum gl_target, GLint tex_ifmt = f.gl_internal_format; GLenum tex_type = f.gl_type; #ifdef __ANDROID__ - android_prepare_tex_upload(s, pixel_data, width, height, + android_prepare_tex_upload(r, s, pixel_data, width, height, depth, upload_row_pitch, upload_slice_pitch, &tex_ifmt, &tex_fmt, &tex_type, - &upload_data, &upload_tmp); + &upload_data); #endif glTexImage3D(gl_target, level, tex_ifmt, width, height, depth, 0, tex_fmt, tex_type, upload_data); -#ifdef __ANDROID__ - g_free(upload_tmp); -#endif } if (converted) { @@ -1205,7 +1273,8 @@ static void upload_gl_texture(GLenum gl_target, #endif } -static TextureBinding* generate_texture(const TextureShape s, +static TextureBinding* generate_texture(PGRAPHGLState *r, + const TextureShape s, const uint8_t *texture_data, const uint8_t *palette_data) { @@ -1282,20 +1351,20 @@ static TextureBinding* generate_texture(const TextureShape s, length = (length + NV2A_CUBEMAP_FACE_ALIGNMENT - 1) & ~(NV2A_CUBEMAP_FACE_ALIGNMENT - 1); - upload_gl_texture(GL_TEXTURE_CUBE_MAP_POSITIVE_X, + upload_gl_texture(r, GL_TEXTURE_CUBE_MAP_POSITIVE_X, s, texture_data + 0 * length, palette_data); - upload_gl_texture(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, + upload_gl_texture(r, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, s, texture_data + 1 * length, palette_data); - upload_gl_texture(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, + upload_gl_texture(r, GL_TEXTURE_CUBE_MAP_POSITIVE_Y, s, texture_data + 2 * length, palette_data); - upload_gl_texture(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, + upload_gl_texture(r, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, s, texture_data + 3 * length, palette_data); - upload_gl_texture(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, + upload_gl_texture(r, GL_TEXTURE_CUBE_MAP_POSITIVE_Z, s, texture_data + 4 * length, palette_data); - upload_gl_texture(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, + upload_gl_texture(r, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, s, texture_data + 5 * length, palette_data); } else { - upload_gl_texture(gl_target, s, texture_data, palette_data); + upload_gl_texture(r, gl_target, s, texture_data, palette_data); } /* Linear textures don't support mipmapping */ @@ -1415,4 +1484,9 @@ void pgraph_gl_finalize_textures(PGRAPHState *pg) free(r->texture_cache_entries); r->texture_cache_entries = NULL; +#ifdef __ANDROID__ + g_free(r->android_tex_conv_buf); + r->android_tex_conv_buf = NULL; + r->android_tex_conv_buf_size = 0; +#endif } diff --git a/hw/xbox/nv2a/pgraph/profile.c b/hw/xbox/nv2a/pgraph/profile.c index 295042010d..a70c27562f 100644 --- a/hw/xbox/nv2a/pgraph/profile.c +++ b/hw/xbox/nv2a/pgraph/profile.c @@ -23,6 +23,11 @@ NV2AStats g_nv2a_stats; void nv2a_profile_increment(void) { +#ifdef __ANDROID__ + /* Skip profiling on Android — stats are never displayed and the + * qemu_clock_get_us() syscalls add unnecessary overhead per frame. */ + return; +#else int64_t now = qemu_clock_get_us(QEMU_CLOCK_REALTIME); const int64_t fps_update_interval = 250000; g_nv2a_stats.last_flip_time = now; @@ -37,10 +42,14 @@ void nv2a_profile_increment(void) ts = now; frame_count = 0; } +#endif } void nv2a_profile_flip_stall(void) { +#ifdef __ANDROID__ + return; +#else int64_t now = qemu_clock_get_us(QEMU_CLOCK_REALTIME); int64_t render_time = (now-g_nv2a_stats.last_flip_time)/1000; @@ -51,6 +60,7 @@ void nv2a_profile_flip_stall(void) (g_nv2a_stats.frame_ptr + 1) % NV2A_PROF_NUM_FRAMES; g_nv2a_stats.frame_count++; memset(&g_nv2a_stats.frame_working, 0, sizeof(g_nv2a_stats.frame_working)); +#endif } const char *nv2a_profile_get_counter_name(unsigned int cnt) diff --git a/ui/xemu.c b/ui/xemu.c index f68d74e7e1..fd31365d07 100644 --- a/ui/xemu.c +++ b/ui/xemu.c @@ -1776,7 +1776,7 @@ void sdl2_gl_refresh(DisplayChangeListener *dcl) #endif #ifdef __ANDROID__ - const int64_t sleep_threshold = 500000; // 0.5ms — Android CFS scheduler jitter is ~0.2ms + const int64_t sleep_threshold = 200000; // 0.2ms — modern Android CFS jitter is <0.1ms #elif !defined(_WIN32) const int64_t sleep_threshold = 2000000; #else @@ -1951,7 +1951,14 @@ static void *call_qemu_main(void *opaque) /* Note: only supports millisecond resolution on Windows */ static void sleep_ns(int64_t ns) { -#ifndef _WIN32 +#ifdef __ANDROID__ + /* Use clock_nanosleep with relative time for better precision on Android. + * CLOCK_MONOTONIC avoids wall-clock adjustments during sleep. */ + struct timespec sleep_delay; + sleep_delay.tv_sec = ns / 1000000000LL; + sleep_delay.tv_nsec = ns % 1000000000LL; + clock_nanosleep(CLOCK_MONOTONIC, 0, &sleep_delay, NULL); +#elif !defined(_WIN32) struct timespec sleep_delay, rem_delay; sleep_delay.tv_sec = ns / 1000000000LL; sleep_delay.tv_nsec = ns % 1000000000LL;