mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-29 04:01:45 +01:00
* 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>
670 lines
23 KiB
C++
670 lines
23 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "mocktail/audio/audio_sink.h"
|
|
#include "mocktail/audio/opensl_simple_buffer_queue.h"
|
|
#include "mocktail/audio/sdl_audio_sink.h"
|
|
|
|
extern "C" std::uint32_t slCreateEngine(void** engine,
|
|
std::uint32_t num_options,
|
|
const void* options,
|
|
std::uint32_t num_interfaces,
|
|
const void* interface_ids,
|
|
const void* interface_required);
|
|
|
|
namespace mocktail::audio {
|
|
namespace {
|
|
|
|
using namespace std::chrono_literals;
|
|
|
|
class ControlledAudioSink final : public AudioSink {
|
|
public:
|
|
const PcmSpec& source_spec() const override { return spec_; }
|
|
|
|
Status Enqueue(const PcmBuffer& buffer) override {
|
|
if (buffer.data == nullptr || buffer.size_bytes == 0) {
|
|
return Status::Error(StatusCode::kInvalidArgument, "empty buffer");
|
|
}
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (stopped_) {
|
|
return Status::Error(StatusCode::kFailedPrecondition, "stopped");
|
|
}
|
|
buffers_.push_back(buffer);
|
|
return Status::Ok();
|
|
}
|
|
|
|
Status Pause() override {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
playing_ = false;
|
|
return stopped_ ? Status::Error(StatusCode::kFailedPrecondition, "stopped")
|
|
: Status::Ok();
|
|
}
|
|
|
|
Status Resume() override {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
playing_ = true;
|
|
return stopped_ ? Status::Error(StatusCode::kFailedPrecondition, "stopped")
|
|
: Status::Ok();
|
|
}
|
|
|
|
Status SetGain(float linear_gain) override {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (stopped_) {
|
|
return Status::Error(StatusCode::kFailedPrecondition, "stopped");
|
|
}
|
|
gain_ = linear_gain;
|
|
return Status::Ok();
|
|
}
|
|
|
|
Status Flush() override { return Status::Ok(); }
|
|
|
|
Status Clear() override {
|
|
ReleaseAll();
|
|
return Status::Ok();
|
|
}
|
|
|
|
Status GetQueuedBytes(std::size_t* size_bytes) const override {
|
|
if (size_bytes == nullptr) {
|
|
return Status::Error(StatusCode::kInvalidArgument, "null output");
|
|
}
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
*size_bytes = 0;
|
|
for (const PcmBuffer& buffer : buffers_) {
|
|
*size_bytes += buffer.size_bytes;
|
|
}
|
|
return Status::Ok();
|
|
}
|
|
|
|
void Shutdown() override {
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
stopped_ = true;
|
|
}
|
|
ReleaseAll();
|
|
}
|
|
|
|
void ReleaseOne() {
|
|
PcmBuffer buffer;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (buffers_.empty()) {
|
|
return;
|
|
}
|
|
buffer = buffers_.front();
|
|
buffers_.erase(buffers_.begin());
|
|
}
|
|
if (buffer.release_callback != nullptr) {
|
|
buffer.release_callback(buffer.release_context, buffer.data,
|
|
buffer.size_bytes);
|
|
}
|
|
}
|
|
|
|
std::size_t buffer_count() const {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
return buffers_.size();
|
|
}
|
|
|
|
bool playing() const {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
return playing_;
|
|
}
|
|
|
|
float gain() const {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
return gain_;
|
|
}
|
|
|
|
private:
|
|
void ReleaseAll() {
|
|
for (;;) {
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (buffers_.empty()) {
|
|
return;
|
|
}
|
|
}
|
|
ReleaseOne();
|
|
}
|
|
}
|
|
|
|
const PcmSpec spec_;
|
|
mutable std::mutex mutex_;
|
|
std::vector<PcmBuffer> buffers_;
|
|
bool playing_ = false;
|
|
float gain_ = 1.0F;
|
|
bool stopped_ = false;
|
|
};
|
|
|
|
struct CallbackProbe {
|
|
std::mutex mutex;
|
|
std::condition_variable cv;
|
|
int calls = 0;
|
|
};
|
|
|
|
void OpenSlQueueCallback(opensl_abi::AndroidSimpleBufferQueue,
|
|
void* context) {
|
|
auto* probe = static_cast<CallbackProbe*>(context);
|
|
{
|
|
std::lock_guard<std::mutex> lock(probe->mutex);
|
|
++probe->calls;
|
|
}
|
|
probe->cv.notify_all();
|
|
}
|
|
|
|
struct ReleaseProbe {
|
|
std::mutex mutex;
|
|
std::condition_variable cv;
|
|
const void* data = nullptr;
|
|
std::size_t size = 0;
|
|
int calls = 0;
|
|
};
|
|
|
|
struct BlockingCallbackProbe {
|
|
std::mutex mutex;
|
|
std::condition_variable cv;
|
|
bool started = false;
|
|
bool release = false;
|
|
};
|
|
|
|
void BlockingOpenSlQueueCallback(opensl_abi::AndroidSimpleBufferQueue,
|
|
void* context) {
|
|
auto* probe = static_cast<BlockingCallbackProbe*>(context);
|
|
std::unique_lock<std::mutex> lock(probe->mutex);
|
|
probe->started = true;
|
|
probe->cv.notify_all();
|
|
probe->cv.wait(lock, [&probe] { return probe->release; });
|
|
}
|
|
|
|
void BufferReleased(void* context, const void* data, std::size_t size) {
|
|
auto* probe = static_cast<ReleaseProbe*>(context);
|
|
{
|
|
std::lock_guard<std::mutex> lock(probe->mutex);
|
|
probe->data = data;
|
|
probe->size = size;
|
|
++probe->calls;
|
|
}
|
|
probe->cv.notify_all();
|
|
}
|
|
|
|
TEST(AudioSpecTest, ValidatesTypedPcmAndFrameSize) {
|
|
const PcmSpec stereo_s16{48000, 2, PcmSampleFormat::kSigned16LittleEndian};
|
|
EXPECT_TRUE(ValidatePcmSpec(stereo_s16).ok());
|
|
EXPECT_EQ(BytesPerFrame(stereo_s16), 4u);
|
|
|
|
PcmSpec invalid = stereo_s16;
|
|
invalid.channels = 0;
|
|
EXPECT_EQ(ValidatePcmSpec(invalid).code(), StatusCode::kInvalidArgument);
|
|
invalid = stereo_s16;
|
|
invalid.sample_rate_hz = 1000;
|
|
EXPECT_EQ(ValidatePcmSpec(invalid).code(), StatusCode::kInvalidArgument);
|
|
}
|
|
|
|
TEST(SdlPlaybackDeviceTest, ResolvesDefaultAndExactPhysicalName) {
|
|
const std::vector<SdlPlaybackDevice> devices = {
|
|
{17, "Built-in Audio Analog Stereo"}, {23, "USB Headset"}};
|
|
std::uint32_t device_id = 99;
|
|
std::string resolved;
|
|
|
|
ASSERT_TRUE(
|
|
ResolveSdlPlaybackDevice("default", devices, &device_id, &resolved).ok());
|
|
EXPECT_EQ(device_id, 0U);
|
|
EXPECT_EQ(resolved, "default");
|
|
|
|
ASSERT_TRUE(
|
|
ResolveSdlPlaybackDevice("USB Headset", devices, &device_id, &resolved)
|
|
.ok());
|
|
EXPECT_EQ(device_id, 23U);
|
|
EXPECT_EQ(resolved, "USB Headset");
|
|
}
|
|
|
|
TEST(SdlPlaybackDeviceTest, RejectsMissingAmbiguousAndUnsafeNames) {
|
|
std::uint32_t device_id = 0;
|
|
std::string resolved;
|
|
EXPECT_EQ(ResolveSdlPlaybackDevice("Missing", {{17, "Speakers"}}, &device_id,
|
|
&resolved)
|
|
.code(),
|
|
StatusCode::kUnavailable);
|
|
EXPECT_EQ(
|
|
ResolveSdlPlaybackDevice("Speakers", {{17, "Speakers"}, {23, "Speakers"}},
|
|
&device_id, &resolved)
|
|
.code(),
|
|
StatusCode::kInvalidArgument);
|
|
EXPECT_EQ(
|
|
ResolveSdlPlaybackDevice("Speaker\nInjected", {}, &device_id, &resolved)
|
|
.code(),
|
|
StatusCode::kInvalidArgument);
|
|
}
|
|
|
|
TEST(SdlAudioSubsystemTest, EnforcesMainThreadOwnershipAndLiveSinkOrder) {
|
|
ASSERT_TRUE(InitializeSdlAudioSubsystem().ok());
|
|
std::vector<SdlPlaybackDevice> devices;
|
|
std::string resolved;
|
|
ASSERT_TRUE(ConfigureSdlPlaybackDevice("default", &devices, &resolved).ok());
|
|
EXPECT_EQ(resolved, "default");
|
|
SdlAudioSinkOptions options;
|
|
std::unique_ptr<AudioSink> sink;
|
|
ASSERT_TRUE(CreateSdlAudioSink(options, &sink).ok());
|
|
EXPECT_EQ(ConfigureSdlPlaybackDevice("default", &devices, &resolved).code(),
|
|
StatusCode::kFailedPrecondition);
|
|
EXPECT_EQ(ShutdownSdlAudioSubsystem().code(),
|
|
StatusCode::kFailedPrecondition);
|
|
|
|
Status worker_shutdown;
|
|
std::thread worker(
|
|
[&worker_shutdown] { worker_shutdown = ShutdownSdlAudioSubsystem(); });
|
|
worker.join();
|
|
EXPECT_EQ(worker_shutdown.code(), StatusCode::kFailedPrecondition);
|
|
|
|
sink->Shutdown();
|
|
sink.reset();
|
|
EXPECT_TRUE(ShutdownSdlAudioSubsystem().ok());
|
|
}
|
|
|
|
TEST(SdlAudioSubsystemTest, OpensExactEnumeratedPlaybackDevice) {
|
|
ASSERT_TRUE(InitializeSdlAudioSubsystem().ok());
|
|
std::vector<SdlPlaybackDevice> devices;
|
|
ASSERT_TRUE(ListSdlPlaybackDevices(&devices).ok());
|
|
if (devices.empty()) {
|
|
EXPECT_TRUE(ShutdownSdlAudioSubsystem().ok());
|
|
GTEST_SKIP() << "SDL audio driver exposed no physical playback device";
|
|
}
|
|
|
|
const std::string requested = devices.front().name;
|
|
std::string resolved;
|
|
ASSERT_TRUE(ConfigureSdlPlaybackDevice(requested, &devices, &resolved).ok());
|
|
EXPECT_EQ(resolved, requested);
|
|
SdlAudioSinkOptions options;
|
|
std::unique_ptr<AudioSink> sink;
|
|
const Status status = CreateSdlAudioSink(options, &sink);
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
sink->Shutdown();
|
|
sink.reset();
|
|
EXPECT_TRUE(ShutdownSdlAudioSubsystem().ok());
|
|
}
|
|
|
|
TEST(SdlAudioSubsystemTest, MigratesLiveStreamWithoutDroppingQueuedPcm) {
|
|
ASSERT_TRUE(InitializeSdlAudioSubsystem().ok());
|
|
std::vector<SdlPlaybackDevice> devices;
|
|
std::string resolved;
|
|
ASSERT_TRUE(ConfigureSdlPlaybackDevice("default", &devices, &resolved).ok());
|
|
|
|
SdlAudioSinkOptions options;
|
|
options.start_paused = true;
|
|
std::unique_ptr<AudioSink> sink;
|
|
ASSERT_TRUE(CreateSdlAudioSink(options, &sink).ok());
|
|
const std::vector<std::int16_t> samples(256 * 2, 91);
|
|
ASSERT_TRUE(sink->Enqueue(PcmBuffer{samples.data(),
|
|
samples.size() * sizeof(samples.front())})
|
|
.ok());
|
|
|
|
if (!devices.empty()) {
|
|
ASSERT_TRUE(SwitchSdlPlaybackDevice(devices.front().id, &resolved).ok());
|
|
EXPECT_EQ(resolved, devices.front().name);
|
|
std::uint32_t configured_id = 0;
|
|
std::string configured_name;
|
|
ASSERT_TRUE(
|
|
GetConfiguredSdlPlaybackDevice(&configured_id, &configured_name).ok());
|
|
EXPECT_EQ(configured_id, devices.front().id);
|
|
EXPECT_EQ(configured_name, devices.front().name);
|
|
}
|
|
|
|
std::size_t queued = 0;
|
|
ASSERT_TRUE(sink->GetQueuedBytes(&queued).ok());
|
|
EXPECT_EQ(queued, samples.size() * sizeof(samples.front()));
|
|
ASSERT_TRUE(SwitchSdlPlaybackDevice(0, &resolved).ok());
|
|
EXPECT_EQ(resolved, "default");
|
|
ASSERT_TRUE(sink->GetQueuedBytes(&queued).ok());
|
|
EXPECT_EQ(queued, samples.size() * sizeof(samples.front()));
|
|
|
|
sink->Shutdown();
|
|
sink.reset();
|
|
EXPECT_TRUE(ShutdownSdlAudioSubsystem().ok());
|
|
}
|
|
|
|
TEST(SdlAudioSinkTest, QueuesRealPcmIntoPausedSdlStream) {
|
|
ASSERT_TRUE(InitializeSdlAudioSubsystem().ok());
|
|
SdlAudioSinkOptions options;
|
|
options.start_paused = true;
|
|
std::unique_ptr<AudioSink> sink;
|
|
Status status = CreateSdlAudioSink(options, &sink);
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
|
|
const std::vector<std::int16_t> samples(512 * 2, 123);
|
|
status = sink->Enqueue(
|
|
PcmBuffer{samples.data(), samples.size() * sizeof(samples.front())});
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
std::size_t queued = 0;
|
|
status = sink->GetQueuedBytes(&queued);
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
EXPECT_EQ(queued, samples.size() * sizeof(samples.front()));
|
|
|
|
status = sink->Clear();
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
ASSERT_TRUE(sink->GetQueuedBytes(&queued).ok());
|
|
EXPECT_EQ(queued, 0u);
|
|
sink->Shutdown();
|
|
EXPECT_EQ(sink->Resume().code(), StatusCode::kFailedPrecondition);
|
|
sink.reset();
|
|
EXPECT_TRUE(ShutdownSdlAudioSubsystem().ok());
|
|
}
|
|
|
|
TEST(SdlAudioSinkTest, ReleasesBorrowedBufferWhenStreamIsCleared) {
|
|
ASSERT_TRUE(InitializeSdlAudioSubsystem().ok());
|
|
SdlAudioSinkOptions options;
|
|
std::unique_ptr<AudioSink> sink;
|
|
Status status = CreateSdlAudioSink(options, &sink);
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
|
|
const std::vector<std::int16_t> samples(128 * 2, 321);
|
|
ReleaseProbe probe;
|
|
status = sink->Enqueue(PcmBuffer{samples.data(),
|
|
samples.size() * sizeof(samples.front()),
|
|
BufferReleased, &probe});
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
ASSERT_TRUE(sink->Clear().ok());
|
|
|
|
std::unique_lock<std::mutex> lock(probe.mutex);
|
|
ASSERT_TRUE(
|
|
probe.cv.wait_for(lock, 2s, [&probe] { return probe.calls == 1; }));
|
|
EXPECT_EQ(probe.data, samples.data());
|
|
EXPECT_EQ(probe.size, samples.size() * sizeof(samples.front()));
|
|
sink->Shutdown();
|
|
sink.reset();
|
|
EXPECT_TRUE(ShutdownSdlAudioSubsystem().ok());
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, DeliversPcmToSinkAndDispatchesCompletion) {
|
|
auto controlled = std::make_unique<ControlledAudioSink>();
|
|
ControlledAudioSink* controlled_view = controlled.get();
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
Status status = OpenSlSimpleBufferQueueAdapter::Create(std::move(controlled),
|
|
{}, &adapter);
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
|
|
auto queue = adapter->interface();
|
|
ASSERT_NE(queue, nullptr);
|
|
CallbackProbe probe;
|
|
ASSERT_EQ((*queue)->RegisterCallback(queue, OpenSlQueueCallback, &probe),
|
|
opensl_abi::kResultSuccess);
|
|
|
|
const std::vector<std::int16_t> samples(64 * 2, 456);
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(),
|
|
static_cast<opensl_abi::Uint32>(
|
|
samples.size() * sizeof(samples.front()))),
|
|
opensl_abi::kResultSuccess);
|
|
EXPECT_EQ(controlled_view->buffer_count(), 1u);
|
|
|
|
opensl_abi::AndroidSimpleBufferQueueState queue_state{};
|
|
ASSERT_EQ((*queue)->GetState(queue, &queue_state),
|
|
opensl_abi::kResultSuccess);
|
|
EXPECT_EQ(queue_state.count, 1u);
|
|
controlled_view->ReleaseOne();
|
|
|
|
{
|
|
std::unique_lock<std::mutex> lock(probe.mutex);
|
|
ASSERT_TRUE(
|
|
probe.cv.wait_for(lock, 2s, [&probe] { return probe.calls == 1; }));
|
|
}
|
|
ASSERT_EQ((*queue)->GetState(queue, &queue_state),
|
|
opensl_abi::kResultSuccess);
|
|
EXPECT_EQ(queue_state.count, 0u);
|
|
EXPECT_EQ(queue_state.index, 1u);
|
|
const OpenSlSimpleBufferQueueStats stats = adapter->GetStats();
|
|
EXPECT_EQ(stats.submitted_buffers, 1u);
|
|
EXPECT_EQ(stats.consumed_buffers, 1u);
|
|
EXPECT_EQ(stats.discarded_buffers, 0u);
|
|
EXPECT_EQ(stats.pending_buffers, 0u);
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, DelegatesLinearGainToTheHostSink) {
|
|
auto controlled = std::make_unique<ControlledAudioSink>();
|
|
ControlledAudioSink* controlled_view = controlled.get();
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
ASSERT_TRUE(OpenSlSimpleBufferQueueAdapter::Create(std::move(controlled), {},
|
|
&adapter)
|
|
.ok());
|
|
|
|
ASSERT_TRUE(adapter->SetGain(0.25F).ok());
|
|
EXPECT_FLOAT_EQ(controlled_view->gain(), 0.25F);
|
|
adapter->Shutdown();
|
|
EXPECT_EQ(adapter->SetGain(1.0F).code(), StatusCode::kFailedPrecondition);
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, SdlCompletionFollowsRealStreamConsumption) {
|
|
ASSERT_TRUE(InitializeSdlAudioSubsystem().ok());
|
|
SdlAudioSinkOptions sink_options;
|
|
sink_options.start_paused = true;
|
|
std::unique_ptr<AudioSink> sink;
|
|
Status status = CreateSdlAudioSink(sink_options, &sink);
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
status =
|
|
OpenSlSimpleBufferQueueAdapter::Create(std::move(sink), {}, &adapter);
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
auto queue = adapter->interface();
|
|
CallbackProbe probe;
|
|
ASSERT_EQ((*queue)->RegisterCallback(queue, OpenSlQueueCallback, &probe),
|
|
opensl_abi::kResultSuccess);
|
|
|
|
const std::vector<std::int16_t> samples(4096 * 2, 222);
|
|
const std::vector<std::int16_t> following_samples(4096 * 2, 333);
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(),
|
|
static_cast<opensl_abi::Uint32>(
|
|
samples.size() * sizeof(samples.front()))),
|
|
opensl_abi::kResultSuccess);
|
|
ASSERT_EQ((*queue)->Enqueue(queue, following_samples.data(),
|
|
static_cast<opensl_abi::Uint32>(
|
|
following_samples.size() *
|
|
sizeof(following_samples.front()))),
|
|
opensl_abi::kResultSuccess);
|
|
|
|
std::this_thread::sleep_for(20ms);
|
|
{
|
|
std::lock_guard<std::mutex> lock(probe.mutex);
|
|
EXPECT_EQ(probe.calls, 0) << "paused SDL stream consumed a borrowed buffer";
|
|
}
|
|
opensl_abi::AndroidSimpleBufferQueueState queue_state{};
|
|
ASSERT_EQ((*queue)->GetState(queue, &queue_state),
|
|
opensl_abi::kResultSuccess);
|
|
EXPECT_EQ(queue_state.count, 2u);
|
|
|
|
status = adapter->SetPlaying(true);
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
{
|
|
std::unique_lock<std::mutex> lock(probe.mutex);
|
|
ASSERT_TRUE(
|
|
probe.cv.wait_for(lock, 2s, [&probe] { return probe.calls >= 1; }));
|
|
}
|
|
ASSERT_EQ((*queue)->GetState(queue, &queue_state),
|
|
opensl_abi::kResultSuccess);
|
|
EXPECT_LT(queue_state.count, 2u);
|
|
EXPECT_GE(queue_state.index, 1u);
|
|
adapter->Shutdown();
|
|
adapter.reset();
|
|
EXPECT_TRUE(ShutdownSdlAudioSubsystem().ok());
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, ClearSuppressesCallbacksForDiscardedBuffers) {
|
|
auto controlled = std::make_unique<ControlledAudioSink>();
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
ASSERT_TRUE(OpenSlSimpleBufferQueueAdapter::Create(std::move(controlled), {},
|
|
&adapter)
|
|
.ok());
|
|
auto queue = adapter->interface();
|
|
CallbackProbe probe;
|
|
ASSERT_EQ((*queue)->RegisterCallback(queue, OpenSlQueueCallback, &probe),
|
|
opensl_abi::kResultSuccess);
|
|
|
|
const std::vector<std::int16_t> samples(32 * 2, 789);
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(),
|
|
static_cast<opensl_abi::Uint32>(
|
|
samples.size() * sizeof(samples.front()))),
|
|
opensl_abi::kResultSuccess);
|
|
ASSERT_EQ((*queue)->Clear(queue), opensl_abi::kResultSuccess);
|
|
std::this_thread::sleep_for(20ms);
|
|
std::lock_guard<std::mutex> lock(probe.mutex);
|
|
EXPECT_EQ(probe.calls, 0);
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, RejectsOverflowAndShutsDownFailClosed) {
|
|
auto controlled = std::make_unique<ControlledAudioSink>();
|
|
OpenSlSimpleBufferQueueOptions options;
|
|
options.max_buffers = 1;
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
ASSERT_TRUE(OpenSlSimpleBufferQueueAdapter::Create(std::move(controlled),
|
|
options, &adapter)
|
|
.ok());
|
|
auto queue = adapter->interface();
|
|
const std::vector<std::int16_t> samples(32 * 2, 111);
|
|
const auto bytes =
|
|
static_cast<opensl_abi::Uint32>(samples.size() * sizeof(samples.front()));
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(), bytes),
|
|
opensl_abi::kResultSuccess);
|
|
EXPECT_EQ((*queue)->Enqueue(queue, samples.data(), bytes),
|
|
opensl_abi::kResultBufferInsufficient);
|
|
adapter->Shutdown();
|
|
EXPECT_EQ((*queue)->Enqueue(queue, samples.data(), bytes),
|
|
opensl_abi::kResultPreconditionsViolated);
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, ShutdownWaitsForInFlightCallback) {
|
|
auto controlled = std::make_unique<ControlledAudioSink>();
|
|
ControlledAudioSink* controlled_view = controlled.get();
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
ASSERT_TRUE(OpenSlSimpleBufferQueueAdapter::Create(std::move(controlled), {},
|
|
&adapter)
|
|
.ok());
|
|
auto queue = adapter->interface();
|
|
BlockingCallbackProbe callback;
|
|
ASSERT_EQ(
|
|
(*queue)->RegisterCallback(queue, BlockingOpenSlQueueCallback, &callback),
|
|
opensl_abi::kResultSuccess);
|
|
|
|
const std::vector<std::int16_t> samples(32 * 2, 444);
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(),
|
|
static_cast<opensl_abi::Uint32>(
|
|
samples.size() * sizeof(samples.front()))),
|
|
opensl_abi::kResultSuccess);
|
|
controlled_view->ReleaseOne();
|
|
{
|
|
std::unique_lock<std::mutex> lock(callback.mutex);
|
|
ASSERT_TRUE(callback.cv.wait_for(lock, 2s,
|
|
[&callback] { return callback.started; }));
|
|
}
|
|
|
|
std::atomic<bool> shutdown_done = false;
|
|
std::thread shutdown_thread([&adapter, &shutdown_done] {
|
|
adapter->Shutdown();
|
|
shutdown_done.store(true);
|
|
});
|
|
std::this_thread::sleep_for(20ms);
|
|
EXPECT_FALSE(shutdown_done.load());
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(callback.mutex);
|
|
callback.release = true;
|
|
}
|
|
callback.cv.notify_all();
|
|
shutdown_thread.join();
|
|
EXPECT_TRUE(shutdown_done.load());
|
|
EXPECT_EQ((*queue)->Enqueue(queue, samples.data(),
|
|
static_cast<opensl_abi::Uint32>(
|
|
samples.size() * sizeof(samples.front()))),
|
|
opensl_abi::kResultPreconditionsViolated);
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, GetStatePollsWhilePlaybackReleasesBuffers) {
|
|
auto controlled = std::make_unique<ControlledAudioSink>();
|
|
ControlledAudioSink* controlled_view = controlled.get();
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
ASSERT_TRUE(OpenSlSimpleBufferQueueAdapter::Create(std::move(controlled), {},
|
|
&adapter)
|
|
.ok());
|
|
auto queue = adapter->interface();
|
|
CallbackProbe probe;
|
|
ASSERT_EQ((*queue)->RegisterCallback(queue, OpenSlQueueCallback, &probe),
|
|
opensl_abi::kResultSuccess);
|
|
|
|
std::atomic<bool> stop_polling{false};
|
|
std::atomic<int> polls{0};
|
|
std::atomic<int> poll_failures{0};
|
|
std::thread poller([&] {
|
|
while (!stop_polling.load(std::memory_order_relaxed)) {
|
|
opensl_abi::AndroidSimpleBufferQueueState queue_state{};
|
|
if ((*queue)->GetState(queue, &queue_state) !=
|
|
opensl_abi::kResultSuccess) {
|
|
poll_failures.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
polls.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
});
|
|
|
|
const std::vector<std::int16_t> samples(32 * 2, 321);
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(),
|
|
static_cast<opensl_abi::Uint32>(
|
|
samples.size() * sizeof(samples.front()))),
|
|
opensl_abi::kResultSuccess);
|
|
opensl_abi::AndroidSimpleBufferQueueState queued{};
|
|
ASSERT_EQ((*queue)->GetState(queue, &queued), opensl_abi::kResultSuccess);
|
|
EXPECT_EQ(queued.count, 1u);
|
|
controlled_view->ReleaseOne();
|
|
{
|
|
std::unique_lock<std::mutex> lock(probe.mutex);
|
|
ASSERT_TRUE(
|
|
probe.cv.wait_for(lock, 2s, [&probe] { return probe.calls == 1; }));
|
|
}
|
|
|
|
stop_polling.store(true, std::memory_order_relaxed);
|
|
poller.join();
|
|
EXPECT_GT(polls.load(std::memory_order_relaxed), 0);
|
|
EXPECT_EQ(poll_failures.load(std::memory_order_relaxed), 0);
|
|
|
|
opensl_abi::AndroidSimpleBufferQueueState drained{};
|
|
ASSERT_EQ((*queue)->GetState(queue, &drained), opensl_abi::kResultSuccess);
|
|
EXPECT_EQ(drained.count, 0u);
|
|
EXPECT_EQ(drained.index, 1u);
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, RecorderIsExplicitlyUnsupported) {
|
|
const Status status = OpenSlRecorderSupportStatus();
|
|
EXPECT_FALSE(status.ok());
|
|
EXPECT_EQ(status.code(), StatusCode::kUnsupported);
|
|
}
|
|
|
|
TEST(LegacyOpenSlStubTest, RequiresExactTestOptIn) {
|
|
unsetenv("MOCKTAIL_ENABLE_TEST_AUDIO_STUBS");
|
|
void* engine = reinterpret_cast<void*>(0x1);
|
|
EXPECT_EQ(slCreateEngine(&engine, 0, nullptr, 0, nullptr, nullptr),
|
|
opensl_abi::kResultFeatureUnsupported);
|
|
EXPECT_EQ(engine, nullptr);
|
|
|
|
setenv("MOCKTAIL_ENABLE_TEST_AUDIO_STUBS", "true", 1);
|
|
engine = reinterpret_cast<void*>(0x1);
|
|
EXPECT_EQ(slCreateEngine(&engine, 0, nullptr, 0, nullptr, nullptr),
|
|
opensl_abi::kResultFeatureUnsupported);
|
|
EXPECT_EQ(engine, nullptr);
|
|
|
|
setenv("MOCKTAIL_ENABLE_TEST_AUDIO_STUBS", "1", 1);
|
|
EXPECT_EQ(slCreateEngine(&engine, 0, nullptr, 0, nullptr, nullptr),
|
|
opensl_abi::kResultSuccess);
|
|
EXPECT_NE(engine, nullptr);
|
|
unsetenv("MOCKTAIL_ENABLE_TEST_AUDIO_STUBS");
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace mocktail::audio
|