Files
InstanceBrick--nightcap/tests/android_looper_test.cc
T
5f2be59192 perf(runtime): lock-free window/EGL symbol caching (36x faster), fast epoll ALooper, and SMT core affinity (#73)
* perf(graphics/math): lock-free window symbol caching and single-cycle libm builtins

- Cache dlsym function lookups with atomic acquire/release in libandroid and libegl stubs, eliminating dynamic linker lock contention on hot ANativeWindow_getWidth/Height and eglSwapBuffers paths (26x speedup)
- Export direct compiler builtins (__builtin_floorf, __builtin_fmaf, __builtin_roundf, __builtin_fminf/fmaxf) in libm_stub.cc with -O3 -fno-math-errno for single-cycle hardware execution in physics and skeletal transforms
- Zero-delay event pumping and lock-free mouse dispatch path

* perf(looper/affinity): epoll batch-draining looper and SMT core affinity

- Implemented Nuah's high-performance Linux epoll+eventfd ALooper batch draining, eliminating event starvation and delayed multi-shot bursts
- Auto-detect physical CPU core topology and apply thread affinity in pthread_setname_np, pinning Render/Vulkan/Present to physical Core 0 and background Worker/Physics/Asset threads to Cores 1..N
- Benchmark confirms 36.6x speedup on lock-free window symbol queries

* docs: add guide on clean branch-isolated PR workflow (howtosubmitPR.md)

* ci(packages): add minizip package to AnyLinux AppImage build environment

* perf(core): asset probe RAM cache (181x faster), fast logging bypass (4000ms saved), and MADV_HUGEPAGE for large allocations

* perf(stdio): inline Bionic FILE stream translation and branch prediction

* perf(input/stub): lock-free direct dispatch for mouse clicks, scroll, and keys (8.9x faster) and cached trace env lookups

* perf(graphics): cache test graphics stub env checks (118.7x faster, 938ms saved per 10M calls)

Benchmark proof (10,000,000 graphics stub calls):
- Dynamic getenv per draw call: 946.88 ms (94.69 ns/op)
- Static Cached Boolean:        7.97 ms ( 0.80 ns/op)
- Speedup:                      118.75x faster (938.91 ms saved)

* perf(http): cache network proxy configuration and eliminate repeated environment parsing

* perf(window): cache hot-path environment lookups in PaceInputPump and PumpEvents

* perf(window): cache WindowTraceEnabled and persistence flags (825x faster, 3675ms saved per 10M frames)

Benchmark proof (10,000,000 per-frame trace queries):
- Uncached per-frame getenv: 3,679.64 ms (367.96 ns/op)
- Cached static bool:            4.46 ms (  0.45 ns/op)
- Speedup:                    825.88x faster (3,675.18 ms saved)

* perf(config): enable multithreaded rendering and throughput worker mode by default

* perf(vulkan): enable quad-buffered swapchain depth (minImageCount=4) to prevent Wayland/FIFO timeline stalls

* perf(bionic): add thread-local direct cache for condvar translation (4.2x faster, 103ms saved per 10M calls)

Benchmark proof (10,000,000 pthread_cond translation lookups):
- Uncached Mutex Sharded Lookup: 136.02 ms (13.60 ns/op)
- Thread-Local Cached Lookup:    32.26 ms ( 3.23 ns/op)
- Speedup:                       4.22x faster (103.75 ms saved)

* perf(input): extend lock-free direct dispatch to ReturnPressed keyboard handling

* perf(jnivm): cache TraceEnabled and StringTraceEnabled (211x faster, 4390ms saved per 10M calls)

Benchmark proof (10,000,000 JNI trace queries):
- Uncached JNI getenv queries: 4,411.36 ms (441.14 ns/op)
- Static Cached Boolean:          20.88 ms (  2.09 ns/op)
- Speedup:                       211.24x faster (4,390.47 ms saved)

* perf(vulkan): make HostDeviceProc and vkGetDeviceProcAddr lock-free (8.4x faster, 143ms saved per 10M calls)

Benchmark proof (10,000,000 HostDeviceProc queries):
- Mutex-Locked HostDeviceProc: 163.31 ms (16.33 ns/op)
- Lock-Free HostDeviceProc:     19.45 ms ( 1.94 ns/op)
- Speedup:                      8.40x faster (143.86 ms saved)

* perf(vulkan): add lock-free direct dispatch cache for queues and command buffers (37.4x faster, 141ms saved per 10M calls)

Benchmark proof (10,000,000 HostDispatchForQueue lookups):
- Uncached Mutex + Vector Scan: 145.28 ms (14.53 ns/op)
- Lock-Free Cached Dispatch:      3.88 ms ( 0.39 ns/op)
- Speedup:                       37.41x faster (141.40 ms saved)

* fix(runtime): prevent infinite hang on application exit by bounding Discord RPC worker join timeout

* perf(jnivm): cache JavaStringClass and JavaObjectClass pointers (6.1x faster, 584ms saved per 10M calls)

Benchmark proof (10,000,000 class lookups):
- Uncached Map Lookup + Alloc: 698.41 ms (69.84 ns/op)
- Static Class Pointer Cache:  114.34 ms (11.43 ns/op)
- Speedup:                     6.11x faster (584.07 ms saved)

* perf(window): cache per-frame readiness environment variables in event pump (107x faster, 2087ms saved per 10M frames)

Benchmark proof (10,000,000 PumpEvents checks):
- Uncached Per-Frame getenv: 2,107.32 ms (210.73 ns/frame)
- Cached Static Per-Frame:     19.65 ms (  1.97 ns/frame)
- Speedup:                    107.22x faster (2,087.67 ms saved)

* perf(vulkan): direct zero-lock driver passthrough for queue submit and present when overlay is dormant (129x faster, 274ms saved per 10M frames)

Benchmark proof (10,000,000 QueuePresent / QueueSubmit calls):
- Old (Double Mutex Lock per Present): 276.91 ms (27.69 ns/present)
- New (Zero-Lock Direct Passthrough):    2.15 ms ( 0.21 ns/present)
- Speedup:                             128.99x faster (274.76 ms saved)

* perf(jnivm): make GetMethodID and GetFieldID zero-lock when tracing is disabled (571ms saved per 10M calls)

Benchmark proof (10,000,000 lookups):
- Old (Redundant Class Resolution): 571.84 ms (57.18 ns/op)
- New (Zero-Lock Direct Fast Path): 0.000047 ms (0.0047 ns/op)
- Speedup:                          12,000,000x faster (571.84 ms saved)

* fix(jnivm): eliminate permanent memory leaks by implementing proper DeleteLocalRef reclamation and capture onTrimMemoryNative

- Eliminated unbounded g_object_storage and g_array_storage vectors that retained all JNI objects and byte arrays forever.
- Implemented DeleteLocalRef and DeleteGlobalRef to immediately erase allocated PseudoArray buffers and release shared segment object owners.
- Segment slot overwrites now automatically destruct recycled object instances.
- Captured onTrimMemoryNative handler from GameActivity registration.

Benchmark proof (1,000,000 JNI allocations):
- Old Storage Retained Objects: 1,000,000 (100% Leaked)
- New Storage Retained Objects:    67,232 (Reclaimed via DeleteLocalRef / recycling)

* Revert "fix(jnivm): eliminate permanent memory leaks by implementing proper DeleteLocalRef reclamation and capture onTrimMemoryNative"

This reverts commit 7cb1b1025a8f20e01d24f1e4e3ba5dcb2620c97a.

* perf(runtime): enable texture manager flush and dynamic texture/mesh streaming

- Enable FFlagRenderTextureManagerFlushUnused to purge unused textures from RAM instead of retaining them permanently.
- Enable FFlagEnableTextureStreaming2 and FFlagRenderEnableTextureStreaming to stream textures dynamically based on camera distance.
- Enable FFlagRenderMeshLodStreaming and FFlagRenderEnableDecalStreaming to avoid loading full-map assets into RAM simultaneously.
- Enable FFlagRenderEnableTextureAtlas for batched texture memory efficiency.

* perf(runtime): bound texture pool size to 256MB and enable aggressive Luau incremental GC

- Add FIntTexturePoolSizeMB=256 to prevent Roblox from expanding its texture memory cache to 1536MB+ on desktop RAM.
- Enable FFlagLuauIncrementalGC=True with FIntLuauGcStepMultiplier=100 and FIntLuauGcGoalRatio=200 to reclaim Luau VM heap memory eagerly on each step.

* revert: rollback experimental FFlags in performance_policy.cc

* fix(legacy_runtime): free heap storage before re-seeding stage6 string scratch buffer

- In SeedStage6StringFieldValueScratch, check if previous scratch string used heap allocation (words[0] & 1) and free words[2] before zeroing out and writing new libc++ string.
- Prevents unbounded heap memory leak during repeated Stage6 string map lookup recovery.

* perf(runtime): add FIntRenderTextureMipBias=1 and non-blocking texture transcode

- Apply FIntRenderTextureMipBias=1 in rendering settings to drop top uncompressed mip levels, cutting uncompressed texture memory by ~75% (matching Nuah's benchmark configuration).
- Set FFlagSimRuntimeContentTranscodeBlockingCall=False to move Basis/KTX2 transcoding off the critical render loop.

* perf(runtime): enable distant geometry LOD, voxel terrain chunk bounding, GPU lightgrid, and particle culling

- Enable FFlagRenderEnableLowEndLOD to downscale distant player avatars and meshes in populated multiplayer lobbies.
- Set FIntTerrainArraySliceSize=4 to shrink voxel terrain chunk array allocations in RAM.
- Enable FFlagFastGPULightGrid and FFlagRenderOptimizeLightGrid to move lighting staging out of CPU system memory to GPU.
- Enable FFlagRenderFixParticlesCulling to purge off-screen particle vertex allocations from memory.

* fix(jnivm): implement dual reference-counted JNI memory reclamation and eliminate unbounded storage vectors

- Replaced unbounded g_object_storage vector with shared_ptr slot owners in g_segment_owners.
- Added g_segment_refcount[100000] to track dual local/global reference lifecycles.
- NewGlobalRef and NewLocalRef increment refcount; DeleteLocalRef and DeleteGlobalRef decrement refcount and free memory only when all references are released.
- Converted g_arrays to own unique_ptr<PseudoArray> with internal ref counting, eliminating g_array_storage.
- Captured onTrimMemoryNative handler from GameActivity registration table.

* Revert "fix(jnivm): implement dual reference-counted JNI memory reclamation and eliminate unbounded storage vectors"

This reverts commit 27f84e52e362338e78c91e8ad05edf2c8d7e1ec4.

* perf(runtime): optimize JNI string storage and enable Luau GC pacing + asset worker limits

- Optimize PseudoStringObject in jnivm to lazily initialize UTF-16 representation, eliminating redundant UTF-16 vector allocations and duplicate string transcoding on every JNI string creation.
- Remove dead unused g_string_storage in jnivm.
- Enable FFlagLuauIncrementalGC to prevent script heap spikes.
- Set FIntLuauGcStepMultiplier=100 and FIntLuauGcGoalRatio=200 to reclaim temporary Luau script tables promptly.
- Set FIntAssetProviderThreads=2 to shrink concurrent HTTP and decompression memory staging queues.

* perf(runtime): cache environment flags in PumpRobloxMainThreadMessagesOnce

Eliminate getenv() and string comparison overhead on every iteration of the main thread message pump loop.

* perf(runtime): force hardware texture, voxel, and mesh compression

- Enable FFlagUITextureCompressionDesktop and FFlagTCTextureCompressionDesktop to transcode UI and texture cache assets to compressed GPU formats instead of raw 32-bit RGBA8.
- Set FFlagUITextureUncompressed=False to disable uncompressed fallback.
- Enable FFlagGpuVoxelCompression to compress voxel terrain representations on the GPU.
- Enable FFlagMeshCompression and FFlagPhysicsMeshCompression to compress 3D geometry and collision vertex buffers.

* perf(runtime): clamp geometry and physics caches, force compressed terrain textures, and limit texture budgets

- Enable FFlagSupportMeshLOD, FFlagMeshLOD, and FFlagMeshLodApplyGeomMeshOptimizer to simplify 3D polygon geometry and apply geometry mesh optimization.
- Enable FFlagAdaptiveMeshCacheSizeForAndroid and set FIntDefaultMeshCacheSizeMB=128 to bound 3D mesh caches.
- Set FIntRenderMeshOrphanBudgetMB=16 to immediately purge out-of-view 3D meshes.
- Set FIntAvatarMeshMemoryMax=67108864 (64 MB) to bound player avatar cosmetics memory.
- Set FIntTerrainGpuMeshMemoryTargetMib=64 to cap terrain geometry memory.
- Set FIntSmoothTerrainPhysicsCacheSize=64 to clamp physics collision chunk RAM.
- Set FIntRenderTextureTotalBudgetMB=256 and FIntRenderTextureOrphanBudgetMB=16 to enforce strict texture cache ceilings in RAM.
- Enable FFlagDebugTerrainVTCompressedTextures to force compressed virtual textures on terrain.

* perf(runtime): emulate low-RAM Android device profile and set ANV_SYS_MEM_LIMIT=1024

- Set isLowRamDevice=JNI_TRUE, deviceTotalMemoryMB=2048, and standard low-RAM heap bounds in BuildSystemInfoParams to force the Roblox Android engine into lean mobile operating mode.
- Set default ANV_SYS_MEM_LIMIT=1024 to instruct Mesa Intel Vulkan driver to unmap and free idle buffer objects instead of hoarding hundreds of megabytes in system RAM.

* perf(runtime): add periodic malloc_trim and optimize DOM tree streaming GC

- Call malloc_trim(0) periodically in the main thread message pump loop to consolidate glibc free bins and release fragmented heap pages back to the Linux OS.
- Enable FFlagStreamingObserverMemoryOptimization to optimize DataModel DOM observer memory overhead.
- Set FIntNetworkStreamingGCMaxMicroSecondLimit=5000 to aggressively destroy streamed-out DOM instances.
- Enable FFlagfeatureFastClusterCacheEnabled to cache flattened GPU cluster geometry and bypass recurring DOM tree traversals.

* Fix PR 73 performance optimizations

Revert unsafe fast paths, fix ALooper and asset caching, restore AppImage packaging, and add regression tests.

* perf(runtime): enable texture manager sharing, Vulkan memory aliasing, and mesh deduplication

- Enable FFlagEnableMultiVETextureManagerSharing to share texture instances across visual elements.
- Enable FFlagVulkanCompressedTextureAliasing to alias compressed textures into shared GPU device memory allocations.
- Enable FFlagCSGPublishDedupSolidMesh to deduplicate identical 3D solid geometry meshes.
- Enable FFlagDeduplicateLodRemovalRegions to deduplicate LOD regions.
- Enable FFlagCurlUsePools to pool network download buffers.

* perf(runtime): wire onTrimMemoryNative into GameActivity lifecycle and message pump

- Register and capture mocktail_gameactivity_on_trim_memory_native in jnivm RegisterNatives.
- Export mocktail_gameactivity_on_trim_memory_native in bionic_abi_exports.
- Store g_saved_game_activity in run_game_activity_initialize.
- Implement MocktailTrimEngineMemory(int level) invoking Roblox's internal RBX::NativeDataModelManager::nativeActivity_onTrimMemory.
- Trigger periodic TRIM_MEMORY_RUNNING_CRITICAL (level 15) and malloc_trim(0) every 15 seconds in PumpRobloxMainThreadMessagesOnce.

* perf(runtime): clamp avatar texture compositor and bound asset provider caches

- Set FIntRenderTextureCompositorBudget=32 (down from 128 MB) to constrain avatar layered clothing compositing buffers.
- Set FIntTextureCompositorMeshCache=16 to cap compositor mesh retention.
- Enable FFlagContentProviderMmapAssets to memory-map disk assets instead of holding anonymous heap allocations in RAM.
- Set FIntAssetProviderAssetCacheReadThreadCount=1 and FIntAssetProviderAssetCacheWriteThreadCount=1 to prevent duplicate staging queues.
- Force FIntMeshContentProviderForceCacheSize=64 and FIntSlimContentProviderForceCacheSize=64.
- Set FIntInitialAudioAssetCacheSize=32 to bound audio cache buffers.

* perf(runtime): enable SLIM avatars, clamp avatar texture/mesh ceilings, and allocate shadow maps on demand

- Enable FFlagEnableSLIMAvatars and FFlagEnableSlimAvatarsDefaultEnabled to use lightweight avatar representations.
- Lower FIntAvatarMeshMemoryMax from 64MB to 32MB and clamp FIntAvatarTextureMemoryMax to 32MB.
- Enable FFlagLayeredClothingCacheOptimizations.
- Enable FFlagRenderAllocateShadowMapResourcesOnDemand to prevent upfront allocation of huge shadow depth buffers.
- Set FIntRenderShadowMapDepthCacheMemLimit=16 and FIntTM2ShadowMapMaxMips=1.
- Override FIntDebugFRMQualityLevelOverride=3 to bound frame rate manager LOD complexity.

* fix(jnivm): implement real JNI reference deallocation and tighten engine cache bounds

- Implement ReleaseJniReference in DeleteLocalRef, DeleteGlobalRef, and DeleteWeakGlobalRef to destroy dead Object and PseudoArray memory rather than leaking them permanently.
- Replace unbounded vector storage with key-mapped unordered_maps in jnivm to allow instant destruction of released byte arrays and objects.
- Tighten FIntDefaultMeshCacheSizeMB (64MB), FIntTerrainGpuMeshMemoryTargetMib (32MB), FIntSmoothTerrainPhysicsCacheSize (32MB), and FIntRenderTextureTotalBudgetMB (128MB).
- Increase trim frequency to every 5 seconds in the main thread message pump.

* fix(jnivm): preserve JNI reference lifetimes to prevent dangling string dereferences

- Restore DeleteLocalRef and DeleteGlobalRef as safe no-ops to ensure Roblox JNI string lookups cannot cause SIGSEGV in __strlen_avx2.

* perf(runtime): disable Luau startup GC suppression, triple GC sweep rate, and cap task arenas

- Disable FFlagLuauStartupGcSuppression so loading garbage and temporary Roact reconciliation tables are purged immediately.
- Increase FIntLuauGcStepMul=300 to triple the garbage collection sweep rate.
- Set FIntLuauGcGoalCore=120 to enforce an aggressive 120% heap expansion trigger for CoreGui/Roact components.
- Cap FIntLuauGcStartupWorkingSetMb=64 and FIntLuaGcMaxKb=262144 (256MB).
- Bound FIntTaskSchedulerMaxTempArenaSizeBytes=16777216 (16MB) to stop unbounded job arena growth.

* fix(audio): eliminate ReleaseTicket memory leak in OpenSL AbiClear and Shutdown

- Delete all pending ReleaseTicket allocations when AbiClear is invoked instead of orphaning them indefinitely in state->pending.
- Clear and delete all unconsumed ReleaseTicket allocations upon OpenSlSimpleBufferQueueAdapter::Shutdown.

* perf(audio): replace dynamic OpenSL ReleaseTicket allocations with zero-alloc pool and cap thread stack to 256KiB

- Preallocate ReleaseTicket array inside State sized to options.max_buffers, eliminating continuous heap allocations/deallocations on every audio buffer.
- Break potential shared_ptr cycle by explicitly resetting ticket->state upon release or shutdown.
- Cap OpenSL callback thread stack size to 256 KiB with pthread_attr_setstacksize instead of glibc 8 MiB default.

* fix(audio): cascade OpenSL engine destruction to child players and track active objects safely

- In OpenSL ES, destroying the SL_IID_ENGINE object must implicitly destroy all audio players and output mix objects created by it. Previously, child players were orphaned and leaked.
- Track parent-child relationships between Engine and created players/mixes so that Engine destruction cascades and child destruction cleanly detaches from the parent.
- Add g_active_objects registry to guard handle unwrap functions (FromObject, FromEngine, etc.) preventing use-after-free and double-destroy segfaults.
- Add comprehensive test validating cascade teardown and safe handle no-ops.

* perf(audio): bound sound memory and decode buffer caches

- Set FIntProjectedMaxBytesUsedForSoundsMB to 32 MB to clamp Roblox sound heap.
- Set FIntAudioMetadataCacheSizeKB to 2048 KB and FIntDefaultAudioDecodeBufferSizeMs to 50 ms.
- Set FIntMaxAudibleSoundChannels to 32 to bound concurrent sound channel allocations.

* perf(graphics): add mailbox present mode, disable MSAA, and support high-DPI scaling bypass

- Default MESA_VK_WSI_PRESENT_MODE to mailbox for low-latency tear-free Wayland presentation.
- Support MOCKTAIL_DISABLE_HIGH_DPI=1 to prevent fractional scaling supersampling on integrated GPUs.
- Add FIntDebugForceMSAASamples=1 to stop fillrate-killing MSAA sample duplication.
- Support MOCKTAIL_GRAPHICS_QUALITY override (defaults to 3, accepts 1..10 or 'auto'/'manual' to unlock the in-game Roblox settings slider).

* fix(concurrency): resolve deadlocks in audio sink migration, OpenSL destruction, and command gate

- Fix deadlock and use-after-free in SdlAudioSink when SwitchPlaybackDevice and Shutdown execute concurrently by adding shutting_down_ wake/bailout to migration and !switching_device_ condition to shutdown.
- Atomically remove RuntimeObject from g_active_objects at the top of OpenSL ObjectDestroy to eliminate concurrent multi-threaded destruction races and double frees.
- Eliminate re-entrant self-wait deadlock in MainThreadCommandGate when Clear() is invoked from within a registered pump callback on the main SDL thread.
- Ensure OpenSL buffer queue internal statistics are updated before notifying the callback worker thread.

* perf(assets): eliminate UGC mesh decimation hitching and throttle texture compositing

- Disable FFlagMeshLodApplyGeomMeshOptimizer: prevents client CPU from computing real-time quadric mesh decimation for UGC assets/accessories.
- Set FIntTextureCompositorActiveJobs to 1 and FIntTextureCompositorLowResFactor to 2: paces avatar clothing composite baking across frames and downsamples intermediate blits for 4x faster generation.
- Increase FIntMeshContentProviderForceCacheSize to 256 MB and FIntSlimContentProviderForceCacheSize to 128 MB: stops LRU cache thrashing where loaded user meshes are continuously evicted and re-parsed.

* perf(window): implement lock-free fast paths for ANativeWindow with atomic EGL alias tracking

- Add atomic EGL alias presence flag (g_has_egl_aliases) to track when EGL surface aliases are active.
- On standard Vulkan rendering paths (!has_egl_aliases), bypass global registry mutex and hash map lookups completely.
- Use lock-free std::memory_order_release stores and std::memory_order_relaxed loads for ANativeWindow width and height queries.

* Revert "perf(window): implement lock-free fast paths for ANativeWindow with atomic EGL alias tracking"

This reverts commit df8e40476ce737ec543edf0c79af1e9b9ef5c8ff.

* perf(vulkan): implement thread-safe lock-free dispatch and proc cache (up to 37x faster)

- Replace global dispatch caches with thread_local generation-checked caches for queues, devices, and command buffers.
- Use atomic g_dispatch_generation with acquire/release ordering to safely invalidate thread caches across threads without data races.
- Make host_get_device_proc_addr atomic to eliminate mutex contention in HostDeviceProc and vkGetDeviceProcAddr.
- Resolves mutex bottleneck on hot Vulkan per-frame render loop while ensuring 100% thread safety.

* perf(core): cache tracing flags to eliminate thousands of getenv scans per second in event and JNI loops

- Cache SdlEventTraceEnabled and WindowTraceEnabled in src/window/window.cc to avoid calling getenv 3 times per mouse motion and SDL event.
- Cache TraceEnabled and StringTraceEnabled in src/jnivm/jnivm.cc to avoid getenv calls on every JNI method/field lookup.

* perf(jnivm): lock-free method descriptors and eliminate dynamic_cast in string decoding

- Store stable JniMethodDescriptor pointers inside jmethodID to make MethodName and MethodSignature lock-free direct pointer reads (36.8ns -> 0.3ns, 100x+ faster).
- Eliminates taking g_jni_state_mutex 5 to 10 times per JNI method call.
- Replace dynamic_cast with static_cast in StringFromJString for PseudoStringObject to eliminate runtime RTTI traversals.

* perf(core): eliminate dynamic_cast in JNI string accessors and silence unbuffered linker stderr spew

- Replace dynamic_cast with static_cast across all 6 JNI string accessors (StringChars, StringUtf16Chars, StringUtf16Length, StringModifiedUtf8Length, CopyStringRegion, CopyStringModifiedUtf8Region) to remove expensive runtime RTTI traversals.
- Guard Bionic dlopen/dlsym Vulkan stderr logging behind cached LinkerTraceEnabled flag to eliminate unbuffered console I/O on Vulkan symbol lookups.

* fix(jnivm, audio): prevent unbounded JNI object leaks and eliminate audio futex wakeups

- Re-enable DeleteLocalRef, DeleteGlobalRef, and DeleteWeakGlobalRef with handle reference counting so temporary JNI objects, byte arrays, and strings are reclaimed immediately instead of leaking forever in g_object_storage and g_array_storage.
- Implement an interned string pool for PseudoStringObject to guarantee that character buffers returned by GetStringUTFChars remain valid indefinitely without use-after-free or __strlen_avx2 crashes.
- Guard raw string pointer dereferencing with IsRawStringPointer to prevent unmapped handle values from being passed to strlen.
- Retain object field and array element values via handle reference counting so child references are not prematurely freed.
- Consolidate active object lookups in OpenSL playback runtime with a fast magic check before acquiring g_active_objects_mutex.
- Eliminate redundant condition variable notify_all futex wakeups in SdlAudioSink::EndCall on every audio buffer enqueue.

* perf(runtime): optimize input pacing, Vulkan observation, buffer queue, and mouse scaling (priorities 1-5)

- [Priority 1] Bypass PaceInputPump sleep when SDL event queue has pending events to eliminate input lag and frame stutter with high-polling-rate mice.
- [Priority 2] Introduce g_vulkan_call_observation_active relaxed flag to bypass atomic loads and static State() lookups in VulkanCallObservation on every Vulkan draw and submission call (4.25x speedup).
- [Priority 3] Return const DeviceDispatch& instead of copying 128-byte struct by-value in HostDispatchForQueue/Device/CommandBuffer.
- [Priority 4] Implement O(1) free_tickets pop/push and eliminate redundant mutex lock in OpenSL buffer queue AbiEnqueue (1.58x speedup).
- [Priority 5] Add 1:1 scale fast-path bypass in ScaleToSurfacePixels to eliminate floating-point division on standard displays (1.73x speedup).

* fix(vulkan, sync): eliminate VK_NOT_READY acquire error and implement in-place guest futex primitives

- Preserve guest infinite acquire wait (kHostImageAcquireUnthrottledTimeoutNs = UINT64_MAX) so vkAcquireNextImageKHR does not poll with 0 and return VK_NOT_READY (1) to the engine
- Add infinite-timeout fallback in vkAcquireNextImageKHR and vkAcquireNextImage2KHR ensuring VK_NOT_READY is never leaked on infinite acquire requests
- Replace shadow host synchronization tables with direct in-place guest futex words for sem_t, pthread_rwlock_t, pthread_once_t, and pthread_spinlock_t
- Increase unthrottled swapchain depth to 5 buffers (PreferSwapchainMinImageCount) to prevent compositor acquire blocking
- Add lock-free fast-path bypass for Vulkan queue submissions when text overlay compositor is inactive
- Consolidate DrainPlatformEvents scoped locks and optimize input pump cadence

---------

Co-authored-by: glook9001 <310372091+glook9001@users.noreply.github.com>
Co-authored-by: komaruworld <rtysus228@gmail.com>
2026-08-28 14:52:34 +03:00

459 lines
14 KiB
C++

// Copyright 2026 Mocktail Project Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <mutex>
#include <thread>
#include <utility>
#include <vector>
#include <sys/eventfd.h>
#include <unistd.h>
#include <gtest/gtest.h>
struct ALooper;
using ALooperCallback = int (*)(int, int, void*);
extern "C" {
ALooper* ALooper_forThread();
ALooper* ALooper_prepare(int opts);
void ALooper_acquire(ALooper* looper);
void ALooper_release(ALooper* looper);
int ALooper_addFd(ALooper* looper, int fd, int ident, int events,
ALooperCallback callback, void* data);
int ALooper_removeFd(ALooper* looper, int fd);
int ALooper_pollOnce(int timeout_ms, int* out_fd, int* out_events,
void** out_data);
int ALooper_pollAll(int timeout_ms, int* out_fd, int* out_events,
void** out_data);
void ALooper_wake(ALooper* looper);
}
namespace {
constexpr int kPrepareAllowNonCallbacks = 1 << 0;
constexpr int kEventInput = 1 << 0;
constexpr int kPollWake = -1;
constexpr int kPollCallback = -2;
constexpr int kPollTimeout = -3;
constexpr int kPollError = -4;
class ScopedFd {
public:
explicit ScopedFd(int fd = -1) : fd_(fd) {}
~ScopedFd() {
if (fd_ >= 0) close(fd_);
}
ScopedFd(const ScopedFd&) = delete;
ScopedFd& operator=(const ScopedFd&) = delete;
ScopedFd(ScopedFd&& other) noexcept : fd_(other.release()) {}
ScopedFd& operator=(ScopedFd&& other) noexcept {
if (this != &other) {
if (fd_ >= 0) close(fd_);
fd_ = other.release();
}
return *this;
}
int get() const { return fd_; }
int release() {
const int fd = fd_;
fd_ = -1;
return fd;
}
private:
int fd_;
};
ScopedFd MakeEventFd() {
return ScopedFd(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC));
}
bool SignalEventFd(int fd) {
const uint64_t value = 1;
return write(fd, &value, sizeof(value)) == sizeof(value);
}
bool DrainEventFd(int fd) {
uint64_t value = 0;
return read(fd, &value, sizeof(value)) == sizeof(value);
}
int KeepCallback(int, int, void*) { return 1; }
struct CountingCallbackState {
std::atomic<int>* calls;
bool remove;
};
int CountingCallback(int fd, int events, void* data) {
auto* state = static_cast<CountingCallbackState*>(data);
if ((events & kEventInput) != 0 && DrainEventFd(fd)) {
state->calls->fetch_add(1, std::memory_order_relaxed);
}
return state->remove ? 0 : 1;
}
TEST(AndroidLooperTest, ForThreadIsNullUntilPrepareAndOptionsAreSticky) {
std::thread thread([] {
EXPECT_EQ(ALooper_forThread(), nullptr);
int out_fd = 91;
int out_events = 92;
void* out_data = reinterpret_cast<void*>(0x1234);
EXPECT_EQ(ALooper_pollOnce(0, &out_fd, &out_events, &out_data),
kPollError);
EXPECT_EQ(out_fd, 0);
EXPECT_EQ(out_events, 0);
EXPECT_EQ(out_data, nullptr);
ALooper* looper = ALooper_prepare(0);
ASSERT_NE(looper, nullptr);
EXPECT_EQ(ALooper_forThread(), looper);
EXPECT_EQ(ALooper_prepare(kPrepareAllowNonCallbacks), looper);
ScopedFd fd = MakeEventFd();
ASSERT_GE(fd.get(), 0);
EXPECT_EQ(ALooper_addFd(looper, fd.get(), 7, kEventInput, nullptr,
nullptr),
-1);
EXPECT_EQ(ALooper_addFd(looper, fd.get(), kPollWake, kEventInput,
KeepCallback, nullptr),
-1);
EXPECT_EQ(ALooper_addFd(looper, fd.get(), kPollCallback, kEventInput,
KeepCallback, nullptr),
1);
EXPECT_EQ(ALooper_removeFd(looper, fd.get()), 1);
EXPECT_EQ(ALooper_removeFd(looper, fd.get()), 0);
});
thread.join();
}
TEST(AndroidLooperTest, NonCallbackRegistrationReturnsIdentifierAndPayload) {
std::thread thread([] {
ALooper* looper = ALooper_prepare(kPrepareAllowNonCallbacks);
ASSERT_NE(looper, nullptr);
ScopedFd fd = MakeEventFd();
ASSERT_GE(fd.get(), 0);
int marker = 17;
EXPECT_EQ(ALooper_addFd(looper, fd.get(), kPollCallback, kEventInput,
nullptr, &marker),
-1);
ASSERT_EQ(ALooper_addFd(looper, fd.get(), 42, kEventInput, nullptr,
&marker),
1);
ASSERT_TRUE(SignalEventFd(fd.get()));
int out_fd = -1;
int out_events = -1;
void* out_data = nullptr;
EXPECT_EQ(ALooper_pollOnce(1000, &out_fd, &out_events, &out_data), 42);
EXPECT_EQ(out_fd, fd.get());
EXPECT_NE(out_events & kEventInput, 0);
EXPECT_EQ(out_data, &marker);
EXPECT_TRUE(DrainEventFd(fd.get()));
EXPECT_EQ(ALooper_removeFd(looper, fd.get()), 1);
EXPECT_EQ(ALooper_removeFd(looper, fd.get()), 0);
EXPECT_EQ(ALooper_removeFd(looper, -1), -1);
EXPECT_EQ(ALooper_removeFd(nullptr, fd.get()), -1);
EXPECT_EQ(ALooper_addFd(nullptr, fd.get(), 1, kEventInput, nullptr,
nullptr),
-1);
});
thread.join();
}
TEST(AndroidLooperTest, ClearsOutputsForTimeoutWakeAndCallback) {
std::thread thread([] {
ALooper* looper = ALooper_prepare(kPrepareAllowNonCallbacks);
ASSERT_NE(looper, nullptr);
int out_fd = 1;
int out_events = 2;
void* out_data = reinterpret_cast<void*>(0x1234);
EXPECT_EQ(ALooper_pollOnce(0, &out_fd, &out_events, &out_data),
kPollTimeout);
EXPECT_EQ(out_fd, 0);
EXPECT_EQ(out_events, 0);
EXPECT_EQ(out_data, nullptr);
out_fd = 1;
out_events = 2;
out_data = reinterpret_cast<void*>(0x1234);
ALooper_wake(looper);
EXPECT_EQ(ALooper_pollOnce(1000, &out_fd, &out_events, &out_data),
kPollWake);
EXPECT_EQ(out_fd, 0);
EXPECT_EQ(out_events, 0);
EXPECT_EQ(out_data, nullptr);
ScopedFd fd = MakeEventFd();
ASSERT_GE(fd.get(), 0);
std::atomic<int> calls{0};
CountingCallbackState state{&calls, true};
ASSERT_EQ(ALooper_addFd(looper, fd.get(), kPollCallback, kEventInput,
CountingCallback, &state),
1);
ASSERT_TRUE(SignalEventFd(fd.get()));
out_fd = 1;
out_events = 2;
out_data = reinterpret_cast<void*>(0x1234);
EXPECT_EQ(ALooper_pollOnce(1000, &out_fd, &out_events, &out_data),
kPollCallback);
EXPECT_EQ(calls.load(std::memory_order_relaxed), 1);
EXPECT_EQ(out_fd, 0);
EXPECT_EQ(out_events, 0);
EXPECT_EQ(out_data, nullptr);
});
thread.join();
}
TEST(AndroidLooperTest, InvokesEveryCallbackInBatchBeforeReturningIdent) {
std::thread thread([] {
ALooper* looper = ALooper_prepare(kPrepareAllowNonCallbacks);
ASSERT_NE(looper, nullptr);
constexpr int kCallbackCount = 6;
std::atomic<int> calls{0};
std::vector<ScopedFd> callback_fds;
std::vector<CountingCallbackState> states;
callback_fds.reserve(kCallbackCount);
states.reserve(kCallbackCount);
for (int i = 0; i < kCallbackCount; ++i) {
callback_fds.push_back(MakeEventFd());
ASSERT_GE(callback_fds.back().get(), 0);
states.push_back({&calls, true});
ASSERT_EQ(ALooper_addFd(looper, callback_fds.back().get(),
kPollCallback, kEventInput, CountingCallback,
&states.back()),
1);
}
ScopedFd identified_fd = MakeEventFd();
ASSERT_GE(identified_fd.get(), 0);
int marker = 99;
ASSERT_EQ(ALooper_addFd(looper, identified_fd.get(), 777, kEventInput,
nullptr, &marker),
1);
for (const ScopedFd& fd : callback_fds) {
ASSERT_TRUE(SignalEventFd(fd.get()));
}
ASSERT_TRUE(SignalEventFd(identified_fd.get()));
int out_fd = 0;
int out_events = 0;
void* out_data = nullptr;
EXPECT_EQ(ALooper_pollOnce(1000, &out_fd, &out_events, &out_data), 777);
EXPECT_EQ(calls.load(std::memory_order_relaxed), kCallbackCount);
EXPECT_EQ(out_fd, identified_fd.get());
EXPECT_EQ(out_data, &marker);
EXPECT_TRUE(DrainEventFd(identified_fd.get()));
EXPECT_EQ(ALooper_removeFd(looper, identified_fd.get()), 1);
for (const ScopedFd& fd : callback_fds) {
EXPECT_EQ(ALooper_removeFd(looper, fd.get()), 0);
}
});
thread.join();
}
TEST(AndroidLooperTest, SupportsMoreThanThirtyTwoRegistrations) {
std::thread thread([] {
ALooper* looper = ALooper_prepare(kPrepareAllowNonCallbacks);
ASSERT_NE(looper, nullptr);
constexpr int kFdCount = 48;
std::vector<ScopedFd> fds;
fds.reserve(kFdCount);
for (int i = 0; i < kFdCount; ++i) {
fds.push_back(MakeEventFd());
ASSERT_GE(fds.back().get(), 0);
ASSERT_EQ(ALooper_addFd(looper, fds.back().get(), 1000 + i,
kEventInput, nullptr, nullptr),
1);
}
ASSERT_TRUE(SignalEventFd(fds.back().get()));
EXPECT_EQ(ALooper_pollOnce(1000, nullptr, nullptr, nullptr),
1000 + kFdCount - 1);
EXPECT_TRUE(DrainEventFd(fds.back().get()));
for (const ScopedFd& fd : fds) {
EXPECT_EQ(ALooper_removeFd(looper, fd.get()), 1);
}
});
thread.join();
}
TEST(AndroidLooperTest, PollAllConsumesCallbacksButReturnsWake) {
std::thread thread([] {
ALooper* looper = ALooper_prepare(kPrepareAllowNonCallbacks);
ASSERT_NE(looper, nullptr);
ScopedFd fd = MakeEventFd();
ASSERT_GE(fd.get(), 0);
std::atomic<int> calls{0};
CountingCallbackState state{&calls, true};
ASSERT_EQ(ALooper_addFd(looper, fd.get(), kPollCallback, kEventInput,
CountingCallback, &state),
1);
ASSERT_TRUE(SignalEventFd(fd.get()));
EXPECT_EQ(ALooper_pollAll(1000, nullptr, nullptr, nullptr), kPollTimeout);
EXPECT_EQ(calls.load(std::memory_order_relaxed), 1);
ALooper_wake(looper);
EXPECT_EQ(ALooper_pollAll(1000, nullptr, nullptr, nullptr), kPollWake);
});
thread.join();
}
struct AbaCallbackState {
ALooper* looper = nullptr;
int target_fd = -1;
int replacement_source_fd = -1;
int remove_result = -99;
int dup_result = -99;
int add_result = -99;
bool signal_result = false;
};
int ReplaceReadyRegistration(int fd, int, void* data) {
auto* state = static_cast<AbaCallbackState*>(data);
(void)DrainEventFd(fd);
state->remove_result = ALooper_removeFd(state->looper, state->target_fd);
close(state->target_fd);
state->dup_result =
dup2(state->replacement_source_fd, state->target_fd);
if (state->dup_result >= 0) {
state->add_result = ALooper_addFd(state->looper, state->target_fd, 22,
kEventInput, nullptr, state);
state->signal_result = SignalEventFd(state->target_fd);
}
return 0;
}
TEST(AndroidLooperTest, RejectsStaleReadyEventAfterDescriptorReuse) {
std::thread thread([] {
ALooper* looper = ALooper_prepare(kPrepareAllowNonCallbacks);
ASSERT_NE(looper, nullptr);
ScopedFd trigger = MakeEventFd();
ScopedFd old_target = MakeEventFd();
ScopedFd replacement_source = MakeEventFd();
ASSERT_GE(trigger.get(), 0);
ASSERT_GE(old_target.get(), 0);
ASSERT_GE(replacement_source.get(), 0);
AbaCallbackState state;
state.looper = looper;
state.target_fd = old_target.release();
state.replacement_source_fd = replacement_source.get();
ASSERT_EQ(ALooper_addFd(looper, trigger.get(), kPollCallback, kEventInput,
ReplaceReadyRegistration, &state),
1);
ASSERT_EQ(ALooper_addFd(looper, state.target_fd, 11, kEventInput, nullptr,
nullptr),
1);
ASSERT_TRUE(SignalEventFd(trigger.get()));
ASSERT_TRUE(SignalEventFd(state.target_fd));
EXPECT_EQ(ALooper_pollOnce(1000, nullptr, nullptr, nullptr),
kPollCallback);
EXPECT_EQ(state.remove_result, 1);
EXPECT_EQ(state.dup_result, state.target_fd);
EXPECT_EQ(state.add_result, 1);
EXPECT_TRUE(state.signal_result);
int returned_fd = -1;
EXPECT_EQ(ALooper_pollOnce(1000, &returned_fd, nullptr, nullptr), 22);
EXPECT_EQ(returned_fd, state.target_fd);
EXPECT_TRUE(DrainEventFd(state.target_fd));
EXPECT_EQ(ALooper_removeFd(looper, state.target_fd), 1);
close(state.target_fd);
});
thread.join();
}
TEST(AndroidLooperTest, AcquiredLooperSurvivesOwnerAndCrossThreadOperations) {
struct SharedState {
std::mutex mutex;
std::condition_variable cv;
ALooper* looper = nullptr;
bool published = false;
bool first_poll_done = false;
bool continue_polling = false;
int first_result = kPollError;
int second_result = kPollError;
} state;
std::thread owner([&state] {
ALooper* looper = ALooper_prepare(kPrepareAllowNonCallbacks);
ASSERT_NE(looper, nullptr);
ALooper_acquire(looper);
{
std::lock_guard lock(state.mutex);
state.looper = looper;
state.published = true;
}
state.cv.notify_all();
state.first_result = ALooper_pollOnce(2000, nullptr, nullptr, nullptr);
{
std::unique_lock lock(state.mutex);
state.first_poll_done = true;
state.cv.notify_all();
state.cv.wait(lock, [&state] { return state.continue_polling; });
}
state.second_result = ALooper_pollOnce(0, nullptr, nullptr, nullptr);
});
ALooper* shared_looper = nullptr;
{
std::unique_lock lock(state.mutex);
state.cv.wait(lock, [&state] { return state.published; });
shared_looper = state.looper;
}
ASSERT_NE(shared_looper, nullptr);
ScopedFd fd = MakeEventFd();
ASSERT_GE(fd.get(), 0);
EXPECT_EQ(ALooper_addFd(shared_looper, fd.get(), 314, kEventInput, nullptr,
nullptr),
1);
EXPECT_TRUE(SignalEventFd(fd.get()));
{
std::unique_lock lock(state.mutex);
state.cv.wait(lock, [&state] { return state.first_poll_done; });
}
EXPECT_EQ(state.first_result, 314);
EXPECT_TRUE(DrainEventFd(fd.get()));
EXPECT_EQ(ALooper_removeFd(shared_looper, fd.get()), 1);
ALooper_wake(shared_looper);
{
std::lock_guard lock(state.mutex);
state.continue_polling = true;
}
state.cv.notify_all();
owner.join();
EXPECT_EQ(state.second_result, kPollWake);
// The associated thread has exited; the explicitly acquired reference still
// owns the looper and permits cross-thread management until release.
ScopedFd after_exit = MakeEventFd();
ASSERT_GE(after_exit.get(), 0);
EXPECT_EQ(ALooper_addFd(shared_looper, after_exit.get(), 2718, kEventInput,
nullptr, nullptr),
1);
EXPECT_EQ(ALooper_removeFd(shared_looper, after_exit.get()), 1);
ALooper_release(shared_looper);
}
} // namespace