Files
InstanceBrick--nightcap/stubs/libandroid_stub.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

1062 lines
31 KiB
C++

// Android NDK compatibility symbols used by libroblox.so on Linux. Asset calls
// can read from an extracted directory or directly from an APK.
//
// Symbols sourced from Android NDK r28 android/native_activity.h,
// android/asset_manager.h, android/looper.h, android/configuration.h.
#include "libc_shim/vulkan_etc1_sky_transcoder.h"
#include <dlfcn.h>
#include <fcntl.h>
#include <minizip/unzip.h>
#if defined(MOCKTAIL_MINIZIP_HAS_STREAM_TELL)
#include <minizip/mz_strm.h>
#endif
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <algorithm>
#include <atomic>
#include <cerrno>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <limits>
#include <mutex>
#include <new>
#include <string>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <unordered_map>
#include <vector>
// Read-only asset access
struct AAssetManager {};
struct AAsset {
std::string path;
std::string apk_entry;
std::vector<unsigned char> data;
size_t offset = 0;
};
struct AAssetDir {};
namespace {
AAssetManager g_asset_manager;
const char* GetEnvNonEmpty(const char* name) {
const char* value = std::getenv(name);
return value != nullptr && value[0] != '\0' ? value : nullptr;
}
bool TraceEnabled() {
static const bool enabled =
GetEnvNonEmpty("MOCKTAIL_ANDROID_STUB_TRACE") != nullptr ||
GetEnvNonEmpty("MOCKTAIL_WINDOW_TRACE") != nullptr ||
GetEnvNonEmpty("MOCKTAIL_FULL_TRACE") != nullptr;
return enabled;
}
bool AssetTraceEnabled() {
static const bool enabled =
GetEnvNonEmpty("MOCKTAIL_ASSET_TRACE") != nullptr ||
GetEnvNonEmpty("MOCKTAIL_TRACE_ALL") != nullptr ||
GetEnvNonEmpty("MOCKTAIL_FULL_TRACE") != nullptr;
return enabled;
}
std::string StripAssetUriPrefix(const char* filename) {
std::string path(filename ? filename : "");
static const char* kPrefixes[] = {
"file:///android_asset/",
"rbxasset://",
};
for (const char* prefix : kPrefixes) {
const size_t prefix_len = std::strlen(prefix);
if (path.compare(0, prefix_len, prefix) == 0) {
return path.substr(prefix_len);
}
}
return path;
}
bool HasUnsafePathSegment(const std::string& path) {
if (path.empty() || path[0] == '/') {
return true;
}
return path == "." || path == ".." ||
path.find("/../") != std::string::npos || path.find("../") == 0 ||
path.rfind("/..") == path.size() - 3 ||
path.find("//") != std::string::npos;
}
std::string StripAndroidAssetPrefix(const std::string& path) {
static const char* kPrefixes[] = {
"assets/",
"content/",
};
for (const char* prefix : kPrefixes) {
const size_t prefix_len = std::strlen(prefix);
if (path.compare(0, prefix_len, prefix) == 0) {
return path.substr(prefix_len);
}
}
return path;
}
bool FileExists(const std::string& path) {
struct stat st;
return !path.empty() && ::stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode);
}
struct AssetLookupCache {
std::mutex mutex;
std::unordered_map<std::string, std::string> resolved_paths;
};
AssetLookupCache g_asset_lookup_cache;
constexpr std::size_t kMaxCachedAssetPaths = 1024 * 5;
std::string AssetCacheKey(const std::string& root,
const std::string& requested) {
std::string key;
key.reserve(root.size() + 1 + requested.size());
key.append(root);
key.push_back('\0');
key.append(requested);
return key;
}
std::string ResolveAssetPath(const char* filename) {
if (filename == nullptr) {
return {};
}
std::string requested = StripAssetUriPrefix(filename);
if (HasUnsafePathSegment(requested)) {
return {};
}
const char* root_env = GetEnvNonEmpty("MOCKTAIL_ASSET_ROOT");
std::string root = root_env != nullptr ? root_env : "rbx_bin/assets";
const std::string cache_key = AssetCacheKey(root, requested);
{
std::scoped_lock lock(g_asset_lookup_cache.mutex);
auto it = g_asset_lookup_cache.resolved_paths.find(cache_key);
if (it != g_asset_lookup_cache.resolved_paths.end()) {
if (FileExists(it->second)) {
return it->second;
}
g_asset_lookup_cache.resolved_paths.erase(it);
}
}
std::string stripped = StripAndroidAssetPrefix(requested);
const std::vector<std::string> candidates = {
root + "/" + requested,
root + "/" + stripped,
root + "/ExtraContent/" + requested,
root + "/ExtraContent/" + stripped,
};
for (const std::string& candidate : candidates) {
if (FileExists(candidate)) {
std::scoped_lock lock(g_asset_lookup_cache.mutex);
if (g_asset_lookup_cache.resolved_paths.size() >=
kMaxCachedAssetPaths &&
g_asset_lookup_cache.resolved_paths.find(cache_key) ==
g_asset_lookup_cache.resolved_paths.end()) {
g_asset_lookup_cache.resolved_paths.erase(
g_asset_lookup_cache.resolved_paths.begin());
}
g_asset_lookup_cache.resolved_paths.insert_or_assign(cache_key,
candidate);
return candidate;
}
}
return {};
}
bool LoadFile(const std::string& path, std::vector<unsigned char>* data) {
if (data == nullptr) {
return false;
}
FILE* file = std::fopen(path.c_str(), "rb");
if (file == nullptr) {
return false;
}
if (std::fseek(file, 0, SEEK_END) != 0) {
std::fclose(file);
return false;
}
long size = std::ftell(file);
if (size < 0) {
std::fclose(file);
return false;
}
if (std::fseek(file, 0, SEEK_SET) != 0) {
std::fclose(file);
return false;
}
data->resize(static_cast<size_t>(size));
if (size > 0) {
size_t read = std::fread(data->data(), 1, static_cast<size_t>(size), file);
if (read != static_cast<size_t>(size)) {
std::fclose(file);
return false;
}
}
std::fclose(file);
return true;
}
std::string ApkEntryName(const char* filename) {
std::string requested = StripAssetUriPrefix(filename);
constexpr const char* kAssetsPrefix = "assets/";
if (requested.compare(0, std::strlen(kAssetsPrefix), kAssetsPrefix) == 0) {
requested.erase(0, std::strlen(kAssetsPrefix));
}
return "assets/" + requested;
}
bool OpenStoredApkEntryDescriptor(const std::string& entry_name, int* fd,
off_t* start, off_t* length) {
const char* apk_path = GetEnvNonEmpty("MOCKTAIL_ASSET_APK_PATH");
if (apk_path == nullptr || fd == nullptr || start == nullptr ||
length == nullptr) {
return false;
}
unzFile archive = unzOpen64(apk_path);
if (archive == nullptr ||
unzLocateFile(archive, entry_name.c_str(), 1) != UNZ_OK) {
if (archive != nullptr) {
unzClose(archive);
}
return false;
}
unz_file_info64 info{};
if (unzGetCurrentFileInfo64(archive, &info, nullptr, 0, nullptr, 0, nullptr,
0) != UNZ_OK ||
info.compression_method != 0 || unzOpenCurrentFile(archive) != UNZ_OK) {
unzClose(archive);
return false;
}
ZPOS64_T data_offset = 0;
#if defined(MOCKTAIL_MINIZIP_HAS_ZSTREAM_POS)
data_offset = unzGetCurrentFileZStreamPos64(archive);
#elif defined(MOCKTAIL_MINIZIP_HAS_STREAM_TELL)
const int64_t stream_offset = mz_stream_tell(unzGetStream(archive));
if (stream_offset < 0) {
unzCloseCurrentFile(archive);
unzClose(archive);
return false;
}
data_offset = static_cast<ZPOS64_T>(stream_offset);
#endif
unzCloseCurrentFile(archive);
unzClose(archive);
if (data_offset > static_cast<ZPOS64_T>(INT64_MAX) ||
info.uncompressed_size > static_cast<ZPOS64_T>(INT64_MAX)) {
return false;
}
const int apk_fd = ::open(apk_path, O_RDONLY | O_CLOEXEC);
if (apk_fd < 0) {
return false;
}
*fd = apk_fd;
*start = static_cast<off_t>(data_offset);
*length = static_cast<off_t>(info.uncompressed_size);
return true;
}
template <typename Fn>
Fn ResolveCached(Fn* slot, const char* name) {
Fn fn = __atomic_load_n(slot, __ATOMIC_ACQUIRE);
if (__builtin_expect(fn == nullptr, 0)) {
fn = reinterpret_cast<Fn>(dlsym(RTLD_DEFAULT, name));
if (fn != nullptr) {
__atomic_store_n(slot, fn, __ATOMIC_RELEASE);
}
}
return fn;
}
void* MocktailNativeWindow() {
using Fn = void* (*)();
static Fn cached_fn = nullptr;
Fn fn = ResolveCached(&cached_fn, "mocktail_native_window");
return fn != nullptr ? fn() : nullptr;
}
int MocktailWindowWidth() {
using Fn = int (*)();
static Fn cached_fn = nullptr;
Fn fn = ResolveCached(&cached_fn, "mocktail_window_width");
return fn != nullptr ? fn() : 1280;
}
int MocktailWindowHeight() {
using Fn = int (*)();
static Fn cached_fn = nullptr;
Fn fn = ResolveCached(&cached_fn, "mocktail_window_height");
return fn != nullptr ? fn() : 720;
}
bool MocktailUsesDirectVulkan() {
using Fn = bool (*)();
static Fn cached_fn = nullptr;
Fn fn = ResolveCached(&cached_fn, "mocktail_window_uses_direct_vulkan");
if (fn != nullptr) {
return fn();
}
const char* backend = GetEnvNonEmpty("MOCKTAIL_GRAPHICS_BACKEND");
return backend != nullptr && std::strcmp(backend, "direct-vulkan") == 0;
}
} // namespace
extern "C" {
AAsset* AAssetManager_open(AAssetManager* mgr, const char* filename,
int) {
if (mgr == nullptr) {
return nullptr;
}
std::string path = ResolveAssetPath(filename);
if (path.empty()) {
return nullptr;
}
auto* asset = new AAsset;
asset->path = path;
asset->apk_entry = ApkEntryName(filename);
if (!LoadFile(path, &asset->data)) {
delete asset;
return nullptr;
}
if (MocktailUsesDirectVulkan()) {
libc_shim::TranscodeEtc1SkyTextureForVulkan(path.c_str(), &asset->data);
}
if (AssetTraceEnabled()) {
std::fprintf(stderr, "[asset] open %s -> %s (%zu bytes)\n", filename,
path.c_str(), asset->data.size());
}
return asset;
}
void AAsset_close(AAsset* asset) {
if (asset != nullptr && AssetTraceEnabled() &&
asset->path.find("shader") != std::string::npos) {
std::fprintf(stderr, "[asset] close %s buffer=%p size=%zu\n",
asset->path.c_str(), asset->data.data(), asset->data.size());
}
delete asset;
}
int AAsset_read(AAsset* asset, void* buf, size_t count) {
if (asset == nullptr || buf == nullptr) {
return -1;
}
const size_t remaining = asset->offset <= asset->data.size()
? asset->data.size() - asset->offset
: 0;
const size_t to_read = std::min(count, remaining);
if (to_read > 0) {
std::memcpy(buf, asset->data.data() + asset->offset, to_read);
asset->offset += to_read;
}
if (AssetTraceEnabled() && asset->path.find("shader") != std::string::npos) {
std::fprintf(stderr, "[asset] read %s count=%zu -> %zu offset=%zu\n",
asset->path.c_str(), count, to_read, asset->offset);
}
return static_cast<int>(to_read);
}
const void* AAsset_getBuffer(AAsset* asset) {
if (asset == nullptr) {
return nullptr;
}
if (AssetTraceEnabled() && asset->path.find("shader") != std::string::npos) {
std::fprintf(stderr, "[asset] getBuffer %s -> %zu bytes\n",
asset->path.c_str(), asset->data.size());
}
return asset->data.empty() ? static_cast<const void*>("")
: static_cast<const void*>(asset->data.data());
}
off_t AAsset_getLength(AAsset* asset) {
if (asset != nullptr && AssetTraceEnabled() &&
asset->path.find("shader") != std::string::npos) {
std::fprintf(stderr, "[asset] getLength %s -> %zu bytes\n",
asset->path.c_str(), asset->data.size());
}
return asset == nullptr ? 0 : static_cast<off_t>(asset->data.size());
}
off_t AAsset_getLength64(AAsset* asset) { return AAsset_getLength(asset); }
off_t AAsset_getRemainingLength(AAsset* asset) {
if (asset == nullptr || asset->offset > asset->data.size()) {
return 0;
}
if (AssetTraceEnabled() && asset->path.find("shader") != std::string::npos) {
std::fprintf(stderr, "[asset] getRemainingLength %s -> %zu bytes\n",
asset->path.c_str(), asset->data.size() - asset->offset);
}
return static_cast<off_t>(asset->data.size() - asset->offset);
}
off_t AAsset_getRemainingLength64(AAsset* asset) {
return AAsset_getRemainingLength(asset);
}
off_t AAsset_seek(AAsset* asset, off_t offset, int whence) {
if (asset == nullptr) {
return -1;
}
off_t base = 0;
if (whence == SEEK_SET) {
base = 0;
} else if (whence == SEEK_CUR) {
base = static_cast<off_t>(asset->offset);
} else if (whence == SEEK_END) {
base = static_cast<off_t>(asset->data.size());
} else {
errno = EINVAL;
return -1;
}
off_t next = base + offset;
if (next < 0) {
errno = EINVAL;
return -1;
}
const auto clamped = std::min(static_cast<size_t>(next), asset->data.size());
asset->offset = clamped;
if (AssetTraceEnabled() && asset->path.find("shader") != std::string::npos) {
std::fprintf(stderr, "[asset] seek %s offset=%lld whence=%d -> %zu\n",
asset->path.c_str(), static_cast<long long>(offset), whence,
asset->offset);
}
return static_cast<off_t>(asset->offset);
}
off_t AAsset_seek64(AAsset* asset, off_t offset, int whence) {
return AAsset_seek(asset, offset, whence);
}
int AAsset_openFileDescriptor(AAsset* asset, off_t* outStart,
off_t* outLength) {
if (asset == nullptr || asset->path.empty()) {
return -1;
}
int fd = -1;
off_t start = 0;
off_t descriptor_length = static_cast<off_t>(asset->data.size());
if (!OpenStoredApkEntryDescriptor(asset->apk_entry, &fd, &start,
&descriptor_length)) {
fd = ::open(asset->path.c_str(), O_RDONLY | O_CLOEXEC);
}
if (fd < 0) {
return -1;
}
if (outStart != nullptr) {
*outStart = start;
}
if (outLength != nullptr) {
*outLength = descriptor_length;
}
if (AssetTraceEnabled() && asset->path.find("shader") != std::string::npos) {
std::fprintf(stderr,
"[asset] openFileDescriptor %s -> fd=%d start=%lld "
"length=%zu\n",
asset->path.c_str(), fd, static_cast<long long>(start),
static_cast<size_t>(descriptor_length));
}
return fd;
}
int AAsset_openFileDescriptor64(AAsset* asset, off_t* outStart,
off_t* outLength) {
return AAsset_openFileDescriptor(asset, outStart, outLength);
}
AAssetManager* AAssetManager_fromJava(void*, void*) {
return &g_asset_manager;
}
// AConfiguration — device configuration query
struct AConfiguration {};
AConfiguration* AConfiguration_new() {
return static_cast<AConfiguration*>(calloc(1, sizeof(AConfiguration)));
}
void AConfiguration_delete(AConfiguration* config) { free(config); }
void AConfiguration_fromAssetManager(AConfiguration*,
AAssetManager*) {}
void AConfiguration_getLanguage(AConfiguration*, char* out) {
if (out) {
out[0] = 'e';
out[1] = 'n';
}
}
void AConfiguration_getCountry(AConfiguration*, char* out) {
if (out) {
out[0] = 'U';
out[1] = 'S';
}
}
int32_t AConfiguration_getScreenSize(AConfiguration*) {
return 2; // ACONFIGURATION_SCREENSIZE_NORMAL
}
int32_t AConfiguration_getScreenWidthDp(AConfiguration*) {
return 360;
}
int32_t AConfiguration_getScreenHeightDp(AConfiguration*) {
return 640;
}
int32_t AConfiguration_getNavHidden(AConfiguration*) {
return 1; // ACONFIGURATION_NAVHIDDEN_YES
}
} // extern "C"
// ALooper — high-performance Linux epoll + eventfd event loop
namespace {
constexpr int kPollWake = -1;
constexpr int kPollCallback = -2;
constexpr int kPollTimeout = -3;
constexpr int kPollError = -4;
constexpr int kPrepareAllowNonCallbacks = 1 << 0;
constexpr uint64_t kWakeToken = 0;
using LooperCallback = int (*)(int, int, void*);
struct LooperRegistration {
int fd = -1;
int ident = 0;
int events = 0;
LooperCallback callback = nullptr;
void* data = nullptr;
uint64_t token = 0;
};
struct LooperResponse {
LooperRegistration registration;
int events = 0;
};
struct ALooper {
explicit ALooper(int requested_opts)
: opts(requested_opts),
allow_non_callbacks(
(requested_opts & kPrepareAllowNonCallbacks) != 0),
epoll_fd(::epoll_create1(EPOLL_CLOEXEC)),
wake_fd(::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)) {
if (epoll_fd < 0 || wake_fd < 0) {
if (epoll_fd >= 0) ::close(epoll_fd);
if (wake_fd >= 0) ::close(wake_fd);
epoll_fd = -1;
wake_fd = -1;
return;
}
epoll_event event{};
event.events = EPOLLIN;
event.data.u64 = kWakeToken;
if (::epoll_ctl(epoll_fd, EPOLL_CTL_ADD, wake_fd, &event) != 0) {
::close(epoll_fd);
::close(wake_fd);
epoll_fd = -1;
wake_fd = -1;
}
}
~ALooper() {
if (epoll_fd >= 0) ::close(epoll_fd);
if (wake_fd >= 0) ::close(wake_fd);
}
ALooper(const ALooper&) = delete;
ALooper& operator=(const ALooper&) = delete;
bool valid() const { return epoll_fd >= 0 && wake_fd >= 0; }
const int opts;
const bool allow_non_callbacks;
int epoll_fd = -1;
int wake_fd = -1;
std::atomic<uint32_t> refs{1};
std::atomic<bool> polling{false};
std::mutex mutex;
std::unordered_map<int, uint64_t> token_by_fd;
std::unordered_map<uint64_t, LooperRegistration> registrations_by_token;
uint64_t next_token = 1;
std::vector<epoll_event> epoll_scratch;
// Only the associated thread accesses pending_responses. Entries are
// generation-checked before being returned so a concurrent remove/re-add
// cannot surface an event for a reused descriptor.
std::deque<LooperResponse> pending_responses;
};
void ReleaseLooperReference(ALooper* looper);
struct ThreadLooperState {
~ThreadLooperState() { ReleaseLooperReference(looper); }
ALooper* looper = nullptr;
};
thread_local ThreadLooperState g_thread_looper;
void ReleaseLooperReference(ALooper* looper) {
if (looper != nullptr &&
looper->refs.fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete looper;
}
}
uint32_t EpollEventsFromAndroid(int events) {
uint32_t result = 0;
if ((events & (1 << 0)) != 0) result |= EPOLLIN;
if ((events & (1 << 1)) != 0) result |= EPOLLOUT;
return result;
}
int AndroidEventsFromEpoll(uint32_t events) {
int result = 0;
if ((events & EPOLLIN) != 0) result |= 1 << 0;
if ((events & EPOLLOUT) != 0) result |= 1 << 1;
if ((events & EPOLLERR) != 0) result |= 1 << 2;
if ((events & EPOLLHUP) != 0) result |= 1 << 3;
return result;
}
uint64_t NextRegistrationTokenLocked(ALooper* looper) {
for (;;) {
const uint64_t token = looper->next_token++;
if (looper->next_token == kWakeToken) {
looper->next_token = 1;
}
if (token != kWakeToken &&
looper->registrations_by_token.find(token) ==
looper->registrations_by_token.end()) {
return token;
}
}
}
bool RegistrationIsCurrent(ALooper* looper,
const LooperRegistration& registration) {
std::scoped_lock lock(looper->mutex);
const auto fd_it = looper->token_by_fd.find(registration.fd);
if (fd_it == looper->token_by_fd.end() ||
fd_it->second != registration.token) {
return false;
}
return looper->registrations_by_token.find(registration.token) !=
looper->registrations_by_token.end();
}
int RemoveFdLocked(ALooper* looper, int fd, uint64_t expected_token) {
const auto fd_it = looper->token_by_fd.find(fd);
if (fd_it == looper->token_by_fd.end() ||
(expected_token != 0 && fd_it->second != expected_token)) {
return 0;
}
const uint64_t token = fd_it->second;
const int ctl_result =
::epoll_ctl(looper->epoll_fd, EPOLL_CTL_DEL, fd, nullptr);
const int ctl_errno = errno;
// Closing an fd automatically removes it from epoll. Treat that as a
// successful logical removal. On a real internal failure, preserve the
// registration so poll and a later removal still agree with the kernel.
if (ctl_result != 0 && ctl_errno != ENOENT && ctl_errno != EBADF) {
errno = ctl_errno;
return -1;
}
looper->token_by_fd.erase(fd_it);
looper->registrations_by_token.erase(token);
return 1;
}
void ClearPollOutputs(int* out_fd, int* out_events, void** out_data) {
if (out_fd != nullptr) *out_fd = 0;
if (out_events != nullptr) *out_events = 0;
if (out_data != nullptr) *out_data = nullptr;
}
bool TakePendingIdent(ALooper* looper, int* out_fd, int* out_events,
void** out_data, int* result) {
while (!looper->pending_responses.empty()) {
const LooperResponse response = looper->pending_responses.front();
looper->pending_responses.pop_front();
if (!RegistrationIsCurrent(looper, response.registration) ||
response.registration.callback != nullptr) {
continue;
}
if (out_fd != nullptr) *out_fd = response.registration.fd;
if (out_events != nullptr) *out_events = response.events;
if (out_data != nullptr) *out_data = response.registration.data;
*result = response.registration.ident;
return true;
}
return false;
}
int RemainingPollTimeout(
int original_timeout,
const std::chrono::steady_clock::time_point& deadline) {
if (original_timeout < 0) return -1;
if (original_timeout == 0) return 0;
const auto remaining = deadline - std::chrono::steady_clock::now();
if (remaining <= std::chrono::steady_clock::duration::zero()) return 0;
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(remaining);
if (millis < remaining) {
millis += std::chrono::milliseconds(1);
}
return static_cast<int>(std::min<int64_t>(
millis.count(), std::numeric_limits<int>::max()));
}
} // namespace
extern "C" {
ALooper* ALooper_forThread() { return g_thread_looper.looper; }
ALooper* ALooper_prepare(int opts) {
if (g_thread_looper.looper != nullptr) {
return g_thread_looper.looper;
}
auto* looper = new (std::nothrow) ALooper(opts);
if (looper == nullptr) {
errno = ENOMEM;
return nullptr;
}
if (!looper->valid()) {
delete looper;
return nullptr;
}
g_thread_looper.looper = looper;
return looper;
}
void ALooper_acquire(ALooper* looper) {
if (looper != nullptr) {
looper->refs.fetch_add(1, std::memory_order_relaxed);
}
}
void ALooper_release(ALooper* looper) {
ReleaseLooperReference(looper);
}
int ALooper_addFd(ALooper* looper, int fd, int ident, int events,
int (*callback)(int, int, void*), void* data) {
const bool callback_ident_valid = ident >= 0 || ident == kPollCallback;
if (looper == nullptr || !looper->valid() || fd < 0 ||
(callback == nullptr &&
(!looper->allow_non_callbacks || ident < 0)) ||
(callback != nullptr && !callback_ident_valid)) {
errno = EINVAL;
return -1;
}
std::scoped_lock lock(looper->mutex);
auto fd_it = looper->token_by_fd.find(fd);
const bool update = fd_it != looper->token_by_fd.end();
const uint64_t old_token = update ? fd_it->second : 0;
const uint64_t token = NextRegistrationTokenLocked(looper);
const LooperRegistration registration = {
fd, callback != nullptr ? kPollCallback : ident, events, callback, data,
token};
const auto inserted =
looper->registrations_by_token.emplace(token, registration);
if (!inserted.second) {
errno = EAGAIN;
return -1;
}
if (update) {
fd_it->second = token;
} else {
const auto fd_inserted = looper->token_by_fd.emplace(fd, token);
if (!fd_inserted.second) {
looper->registrations_by_token.erase(token);
errno = EAGAIN;
return -1;
}
fd_it = fd_inserted.first;
}
epoll_event event{};
event.events = EpollEventsFromAndroid(events);
event.data.u64 = token;
int operation = update ? EPOLL_CTL_MOD : EPOLL_CTL_ADD;
int ctl_result = ::epoll_ctl(looper->epoll_fd, operation, fd, &event);
if (ctl_result != 0 && update && errno == ENOENT) {
operation = EPOLL_CTL_ADD;
ctl_result = ::epoll_ctl(looper->epoll_fd, operation, fd, &event);
} else if (ctl_result != 0 && !update && errno == EEXIST) {
operation = EPOLL_CTL_MOD;
ctl_result = ::epoll_ctl(looper->epoll_fd, operation, fd, &event);
}
if (ctl_result != 0) {
const int ctl_errno = errno;
looper->registrations_by_token.erase(token);
if (update) {
fd_it->second = old_token;
} else {
looper->token_by_fd.erase(fd_it);
}
errno = ctl_errno;
return -1;
}
if (update) {
looper->registrations_by_token.erase(old_token);
}
return 1;
}
int ALooper_removeFd(ALooper* looper, int fd) {
if (looper == nullptr || !looper->valid() || fd < 0) {
errno = EINVAL;
return -1;
}
std::scoped_lock lock(looper->mutex);
return RemoveFdLocked(looper, fd, 0);
}
void ALooper_wake(ALooper* looper) {
if (looper == nullptr || looper->wake_fd < 0) return;
const uint64_t value = 1;
ssize_t written;
do {
written = ::write(looper->wake_fd, &value, sizeof(value));
} while (written < 0 && errno == EINTR);
}
bool ALooper_isPolling(ALooper* looper) {
return looper != nullptr &&
looper->polling.load(std::memory_order_acquire);
}
int ALooper_pollOnce(int timeout_ms, int* out_fd, int* out_events,
void** out_data) {
ClearPollOutputs(out_fd, out_events, out_data);
auto* looper = ALooper_forThread();
if (looper == nullptr || !looper->valid()) return kPollError;
int pending_result = 0;
if (TakePendingIdent(looper, out_fd, out_events, out_data,
&pending_result)) {
return pending_result;
}
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds(
timeout_ms > 0 ? timeout_ms : 0);
int wait_timeout = timeout_ms;
for (;;) {
int max_events = 0;
{
std::scoped_lock lock(looper->mutex);
const std::size_t event_capacity =
1 + looper->registrations_by_token.size();
if (event_capacity >
static_cast<std::size_t>(std::numeric_limits<int>::max())) {
errno = EOVERFLOW;
return kPollError;
}
if (looper->epoll_scratch.size() < event_capacity) {
looper->epoll_scratch.resize(event_capacity);
}
max_events = static_cast<int>(looper->epoll_scratch.size());
}
looper->polling.store(true, std::memory_order_release);
const int ready = ::epoll_wait(looper->epoll_fd,
looper->epoll_scratch.data(), max_events,
wait_timeout);
const int wait_errno = errno;
looper->polling.store(false, std::memory_order_release);
if (ready == 0) return kPollTimeout;
if (ready < 0) {
if (wait_errno != EINTR) {
errno = wait_errno;
return kPollError;
}
wait_timeout = RemainingPollTimeout(timeout_ms, deadline);
continue;
}
bool had_wake = false;
std::vector<LooperResponse> responses;
responses.reserve(static_cast<std::size_t>(ready));
for (int i = 0; i < ready; ++i) {
const epoll_event& event =
looper->epoll_scratch[static_cast<std::size_t>(i)];
if (event.data.u64 != kWakeToken) {
continue;
}
uint64_t value = 0;
ssize_t read_result;
do {
read_result = ::read(looper->wake_fd, &value, sizeof(value));
} while (read_result < 0 && errno == EINTR);
had_wake = true;
}
{
std::scoped_lock lock(looper->mutex);
for (int i = 0; i < ready; ++i) {
const epoll_event& event =
looper->epoll_scratch[static_cast<std::size_t>(i)];
const uint64_t token = event.data.u64;
if (token == kWakeToken) {
continue;
}
const auto registration_it =
looper->registrations_by_token.find(token);
if (registration_it == looper->registrations_by_token.end()) {
continue;
}
responses.push_back({registration_it->second,
AndroidEventsFromEpoll(event.events)});
}
}
bool invoked_callback = false;
for (const LooperResponse& response : responses) {
if (response.registration.callback == nullptr) continue;
const int keep = response.registration.callback(
response.registration.fd, response.events,
response.registration.data);
invoked_callback = true;
if (keep == 0) {
std::scoped_lock lock(looper->mutex);
(void)RemoveFdLocked(looper, response.registration.fd,
response.registration.token);
}
}
for (const LooperResponse& response : responses) {
if (response.registration.callback == nullptr) {
looper->pending_responses.push_back(response);
}
}
if (TakePendingIdent(looper, out_fd, out_events, out_data,
&pending_result)) {
return pending_result;
}
if (invoked_callback) return kPollCallback;
if (had_wake) return kPollWake;
// Every event in the batch belonged to a removed or replaced
// registration. Keep waiting without extending the caller's timeout.
wait_timeout = RemainingPollTimeout(timeout_ms, deadline);
}
}
int ALooper_pollAll(int timeout_ms, int* out_fd, int* out_events,
void** out_data) {
int current_timeout = timeout_ms;
for (;;) {
const int result =
ALooper_pollOnce(current_timeout, out_fd, out_events, out_data);
if (result != kPollCallback) return result;
current_timeout = 0;
}
}
// ANativeWindow — surface / window handle
struct ANativeWindow {};
void ANativeWindow_acquire(ANativeWindow*) {}
void ANativeWindow_release(ANativeWindow*) {}
ANativeWindow* ANativeWindow_fromSurface(void* env, void* surface) {
void* native_window = MocktailNativeWindow();
// The direct-Vulkan WSI adapter ignores the Android window
// member and creates VkSurfaceKHR from SDL_Window. Preserve the Java Surface
// token as its opaque ANativeWindow identity so a recreated Surface is not
// collapsed back onto the same SDL_Window pointer inside Roblox.
if (surface != nullptr && MocktailUsesDirectVulkan()) {
if (TraceEnabled()) {
std::fprintf(stderr,
"[androidstub] ANativeWindow_fromSurface env=%p surface=%p "
"-> surface_identity=%p backend_window=%p\n",
env, surface, surface, native_window);
}
return reinterpret_cast<ANativeWindow*>(surface);
}
if (native_window != nullptr) {
if (TraceEnabled()) {
std::fprintf(stderr,
"[androidstub] ANativeWindow_fromSurface env=%p surface=%p "
"-> native_window=%p\n",
env, surface, native_window);
}
return reinterpret_cast<ANativeWindow*>(native_window);
}
static ANativeWindow fallback_window;
if (TraceEnabled()) {
std::fprintf(stderr,
"[androidstub] ANativeWindow_fromSurface env=%p surface=%p "
"-> fallback_window=%p\n",
env, surface, static_cast<void*>(&fallback_window));
}
return &fallback_window;
}
int32_t ANativeWindow_getWidth(ANativeWindow* window) {
int width = MocktailWindowWidth();
if (TraceEnabled()) {
std::fprintf(stderr,
"[androidstub] ANativeWindow_getWidth window=%p -> %d\n",
static_cast<void*>(window), width);
}
return width;
}
int32_t ANativeWindow_getHeight(ANativeWindow* window) {
int height = MocktailWindowHeight();
if (TraceEnabled()) {
std::fprintf(stderr,
"[androidstub] ANativeWindow_getHeight window=%p -> %d\n",
static_cast<void*>(window), height);
}
return height;
}
} // extern "C"