Fixes #128 Fixes #132
This commit is contained in:
komaruworld committed 2026-09-14 16:36:35 +03:00
1 parent 5be2f68a55
commit 537828acc2
7 files changed
+547 -10

No files matched your search

@@ -0,0 +1,64 @@
#ifndef MOCKTAIL_AUDIO_FMOD_THREAD_FLOATING_POINT_H_
#define MOCKTAIL_AUDIO_FMOD_THREAD_FLOATING_POINT_H_
#include <string_view>
#if defined(__x86_64__)
#include <xmmintrin.h>
#endif
namespace mocktail::audio {
// FMOD's Android threads identify themselves through JavaVMAttachArgs before
// entering their mix loop. Voice DSP filters can accumulate subnormal values
// and spend an entire x86 core on floating-point assists (issue #128).
// Limit FTZ/DAZ to these audio threads; retain the caller's other FP controls.
class FmodThreadFloatingPointMode final {
public:
FmodThreadFloatingPointMode() = default;
~FmodThreadFloatingPointMode() { Restore(); }
FmodThreadFloatingPointMode(const FmodThreadFloatingPointMode&) = delete;
FmodThreadFloatingPointMode& operator=(const FmodThreadFloatingPointMode&) =
delete;
bool Enable(const char* thread_name) noexcept {
#if defined(__x86_64__)
if (active_ || thread_name == nullptr) return false;
const std::string_view name(thread_name);
if (name != "FMOD mixer thread" && name != "FMOD feeder thread" &&
name != "FMOD Convolution thread" && name != "FMOD Worker Thread") {
return false;
}
const unsigned int control = _mm_getcsr();
original_mode_ = control & kModeMask;
_mm_setcsr(control | kModeMask);
active_ = true;
return true;
#else
(void)thread_name;
return false;
#endif
}
void Restore() noexcept {
#if defined(__x86_64__)
if (active_) {
// Restore only our two bits, preserving rounding, exception masks and
// status flags set by the guest while the thread was attached.
_mm_setcsr((_mm_getcsr() & ~kModeMask) | original_mode_);
active_ = false;
}
#endif
}
private:
#if defined(__x86_64__)
static constexpr unsigned int kModeMask = (1U << 15) | (1U << 6);
unsigned int original_mode_ = 0;
bool active_ = false;
#endif
};
} // namespace mocktail::audio
#endif // MOCKTAIL_AUDIO_FMOD_THREAD_FLOATING_POINT_H_
@@ -4,6 +4,7 @@
#include <array>
#include <cstdint>
#include <mutex>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
@@ -13,13 +14,15 @@
namespace mocktail::audio {
class NativeInputCapture;
// Replaces the profiled input/output-device query/select slots of the exact
// Build-ID-scoped FmodAudioDevice vtable. Roblox keeps owning its FMOD engine,
// while the existing settings UI sees and selects the SDL host routes that
// consume Android AudioTrack PCM.
class RobloxOutputDeviceBridge final {
public:
RobloxOutputDeviceBridge() = default;
RobloxOutputDeviceBridge();
~RobloxOutputDeviceBridge();
RobloxOutputDeviceBridge(const RobloxOutputDeviceBridge&) = delete;
@@ -58,6 +61,9 @@ class RobloxOutputDeviceBridge final {
void SelectDevice(int index, bool input = false);
void ConstructGuestString(void* destination, std::string_view value) const;
std::unique_ptr<NativeInputCapture> capture_;
bool capture_profile_supported_ = false;
std::array<std::uintptr_t, 6> original_capture_methods_{};
mutable std::mutex mutex_;
compat::FmodOutputDeviceBridgeProfile profile_;
std::vector<MenuDevice> devices_;
+225
View File
@@ -0,0 +1,225 @@
#ifndef MOCKTAIL_AUDIO_NATIVE_INPUT_CAPTURE_H_
#define MOCKTAIL_AUDIO_NATIVE_INPUT_CAPTURE_H_
#include <algorithm>
#include <array>
#include <atomic>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <map>
#include <memory>
#include <mutex>
#include <vector>
#include "mocktail/audio/sdl_audio_capture.h"
namespace mocktail::audio {
class NativeInputCapture;
struct NativePcmFormat {
std::uint32_t rate = 48000;
std::uint8_t channels = 1;
std::uint8_t bytes_per_sample = 2;
std::uint8_t flags = 0;
std::uint8_t padding = 0;
};
struct NativeOptionalFormat {
NativePcmFormat format{};
std::uint64_t present = 0;
};
struct NativeSharedSink { void* object; void* owner; };
static_assert(sizeof(NativeOptionalFormat) == 16);
static_assert(sizeof(NativePcmFormat) == 8);
inline std::atomic<NativeInputCapture*> g_native_capture{nullptr};
class NativeInputCapture {
public:
using DeliverPcm = std::size_t (*)(void*, const void*, std::size_t,
std::uint64_t, const NativePcmFormat*);
using Destroy = void (*)(NativeSharedSink*);
using OriginalLatency = void (*)(void*, std::uint32_t*, std::uint32_t*);
struct SinkAbi {
std::uintptr_t vtable;
DeliverPcm deliver;
Destroy release;
OriginalLatency latency;
};
NativeInputCapture(SinkAbi abi, bool enabled)
: abi_(abi), enabled_(enabled) {}
~NativeInputCapture() { Shutdown(); }
static NativeOptionalFormat Format(void*) {
auto* capture = g_native_capture.load(std::memory_order_acquire);
return {{}, capture && capture->enabled_ ? 1ULL : 0ULL};
}
static void Latency(void* self, std::uint32_t* playback,
std::uint32_t* recording) {
// Preserve the native playback latency and report the host capture queue.
auto* capture = g_native_capture.load(std::memory_order_acquire);
if (!capture) return;
capture->abi_.latency(self, playback, recording);
if (recording) {
std::lock_guard lock(capture->mutex_);
*recording = 10;
auto it = capture->sessions_.find(self);
if (it != capture->sessions_.end()) {
std::lock_guard queue_lock(it->second->queue_mutex);
*recording += static_cast<std::uint32_t>(it->second->count * 10);
}
}
}
static bool Start(void* self, NativeSharedSink* sink) {
auto* capture = g_native_capture.load(std::memory_order_acquire);
return capture && capture->StartSink(self, sink);
}
static void Stop(void* self, NativeSharedSink* sink) {
auto* capture = g_native_capture.load(std::memory_order_acquire);
if (capture) capture->StopSink(self, sink);
}
static bool Recording(void* self) {
auto* capture = g_native_capture.load(std::memory_order_acquire);
if (!capture) return false;
std::lock_guard lock(capture->mutex_);
return capture->sessions_.find(self) != capture->sessions_.end();
}
static void Poll(void* self) {
auto* capture = g_native_capture.load(std::memory_order_acquire);
if (capture) capture->Deliver(self);
}
void Shutdown() {
std::lock_guard lock(mutex_);
for (auto& [self, session] : sessions_) Close(*session);
sessions_.clear();
}
private:
struct Session {
std::unique_ptr<AudioCapture> input;
std::vector<NativeSharedSink> sinks;
std::mutex queue_mutex;
// At most 200 ms; a stalled consumer must not accumulate stale speech.
std::array<std::array<std::int16_t, 480>, 20> frames{};
std::size_t head = 0, count = 0;
std::uint64_t delivered = 0, dropped = 0;
bool audible = false;
};
static void OnData(void* context, std::size_t bytes) {
auto& session = *static_cast<Session*>(context);
if (bytes != sizeof(session.frames[0])) return;
std::lock_guard lock(session.queue_mutex);
if (session.count == session.frames.size()) {
session.head = (session.head + 1) % session.frames.size();
--session.count;
++session.dropped;
}
std::memcpy(session.frames[(session.head + session.count) %
session.frames.size()].data(),
session.input->buffer_data(), bytes);
++session.count;
}
bool StartSink(void* self, NativeSharedSink* sink) {
std::lock_guard lock(mutex_);
if (!enabled_ || !self || !sink || !sink->object || !sink->owner)
return false;
auto* table = *reinterpret_cast<std::uintptr_t**>(sink->object);
if (reinterpret_cast<std::uintptr_t>(table) != abi_.vtable ||
table[2] != reinterpret_cast<std::uintptr_t>(abi_.deliver)) {
std::fprintf(stderr, " [audio-input] unsupported native PCM sink ABI\n");
return false;
}
auto it = sessions_.find(self);
if (it != sessions_.end()) {
for (const auto& existing : it->second->sinks)
if (existing.object == sink->object) return true;
} else {
auto session = std::make_unique<Session>();
SdlAudioCaptureOptions options;
options.output_spec.channels = 1;
options.data_callback = &OnData;
options.data_context = session.get();
auto status = CreateSdlAudioCapture(options, &session->input);
if (status.ok()) status = session->input->Start();
if (!status.ok()) {
std::fprintf(stderr, " [audio-input] capture start failed: %s\n",
status.message().c_str());
return false;
}
it = sessions_.emplace(self, std::move(session)).first;
std::fprintf(stderr,
" [audio-input] native capture started: 48000 Hz mono signed16\n");
}
// The native parameter is a by-value shared_ptr. Retain its existing
// ownership by moving it, leaving the caller's temporary empty.
it->second->sinks.push_back(*sink);
*sink = {};
return true;
}
void Release(NativeSharedSink& sink) {
abi_.release(&sink);
sink = {};
}
void Close(Session& session) {
session.input->Shutdown();
for (auto& sink : session.sinks) Release(sink);
session.sinks.clear();
std::fprintf(stderr,
" [audio-input] native capture stopped: delivered=%llu dropped=%llu\n",
static_cast<unsigned long long>(session.delivered),
static_cast<unsigned long long>(session.dropped));
}
void StopSink(void* self, NativeSharedSink* sink) {
std::lock_guard lock(mutex_);
auto it = sessions_.find(self);
if (it == sessions_.end() || !sink) return;
auto& session = *it->second;
auto existing = std::find_if(session.sinks.begin(), session.sinks.end(),
[&](const auto& entry) { return entry.object == sink->object; });
if (existing == session.sinks.end()) return;
Release(*existing);
session.sinks.erase(existing);
if (session.sinks.empty()) {
Close(session);
sessions_.erase(it);
}
}
void Deliver(void* self) {
// Invoked by Roblox's recording thread. Guest code and allocator TLS must
// never run on SDL's capture callback thread.
std::lock_guard lock(mutex_);
auto it = sessions_.find(self);
if (it == sessions_.end()) return;
auto& session = *it->second;
for (std::size_t n = 0; n < session.frames.size(); ++n) {
std::array<std::int16_t, 480> pcm;
std::uint64_t delay;
{
std::lock_guard queue_lock(session.queue_mutex);
if (!session.count) break;
pcm = session.frames[session.head];
delay = session.count * 10000000ULL;
session.head = (session.head + 1) % session.frames.size();
--session.count;
}
const NativePcmFormat format;
for (const auto& sink : session.sinks)
abi_.deliver(
sink.object, pcm.data(), pcm.size(), delay, &format);
if (++session.delivered == 1)
std::fprintf(stderr, " [audio-input] PCM delivered to native voice sink\n");
if (!session.audible && std::any_of(pcm.begin(), pcm.end(),
[](auto value) { return value != 0; })) {
session.audible = true;
std::fprintf(stderr, " [audio-input] nonzero microphone PCM delivered\n");
}
}
}
SinkAbi abi_;
bool enabled_;
std::mutex mutex_;
std::map<void*, std::unique_ptr<Session>> sessions_;
};
} // namespace mocktail::audio
#endif
+67 -3
View File
@@ -21,6 +21,7 @@
#include <vector>
#include "audio/roblox_output_device_bridge_internal.h"
#include "audio/native_input_capture.h"
#include "linker/linker.h"
#include "mocktail/audio/sdl_audio_capture.h"
#include "mocktail/audio/sdl_audio_sink.h"
@@ -105,7 +106,7 @@ bool SetVtableWritable(std::uintptr_t* vtable, std::size_t select_index,
}
const std::uintptr_t page_size = static_cast<std::uintptr_t>(page_size_value);
const std::uintptr_t first =
reinterpret_cast<std::uintptr_t>(vtable + kOutputCountVtableIndex);
reinterpret_cast<std::uintptr_t>(vtable + 4);
const std::uintptr_t last =
reinterpret_cast<std::uintptr_t>(vtable + select_index + 1);
if (last <= first) {
@@ -129,6 +130,16 @@ std::uintptr_t FunctionAddress(Function function) {
} // namespace
// ABI verified for Roblox 2.738.1397. Keep capture separate from the menu
// profile: relocating query methods alone does not establish a recording ABI.
namespace {
constexpr std::array<std::size_t, 6> kCaptureSlots{4, 13, 14, 15, 16, 17};
constexpr std::array<std::uintptr_t, 6> kCaptureRvas{
0x320c630, 0x320bcf6, 0x320d4b4, 0x320dd94, 0x320b844, 0x320e49a};
} // namespace
RobloxOutputDeviceBridge::RobloxOutputDeviceBridge() = default;
namespace internal {
bool HasExpectedFmodStringConstructorContract(const std::uint8_t* code,
@@ -215,6 +226,10 @@ Status RobloxOutputDeviceBridge::Install(const compat::BuildProfile& profile) {
"another Roblox output-device bridge owns the process");
}
profile_ = *profile.fmod_output_device_bridge;
capture_profile_supported_ =
profile.elf_build_id == "5f0704edd9064f566ee3d6df2bd2fabbcc709f03" &&
profile_.vtable_rva == 0x6cd3ce0 &&
profile_.vtable_layout_version == 2 && profile_.has_input_devices();
if (!linker::SetAndroidLibraryLoadObserver(&ObserveAndroidLibrary, this)) {
g_active_bridge.store(nullptr, std::memory_order_release);
profile_ = {};
@@ -240,6 +255,8 @@ void RobloxOutputDeviceBridge::Shutdown() {
" [audio-menu] failed to restore FmodAudioDevice "
"vtable protection\n");
}
g_native_capture.store(nullptr, std::memory_order_release);
capture_.reset();
active_ = false;
devices_.clear();
input_devices_.clear();
@@ -408,9 +425,9 @@ Status RobloxOutputDeviceBridge::PatchVtableLocked() {
return FailedPrecondition("FMOD output-device vtable range overflows");
}
const std::uintptr_t first_slot_rva =
profile_.vtable_rva + kOutputCountVtableIndex * sizeof(std::uintptr_t);
profile_.vtable_rva + 4 * sizeof(std::uintptr_t);
const std::size_t slot_range_size =
(kSelectOutputVtableIndex - kOutputCountVtableIndex + 1) *
(kSelectOutputVtableIndex - 4 + 1) *
sizeof(std::uintptr_t);
if (!IsRelroImageRange(library_base_, first_slot_rva, slot_range_size) ||
!IsExecutableImageRange(library_base_, profile_.string_constructor_rva,
@@ -444,6 +461,29 @@ Status RobloxOutputDeviceBridge::PatchVtableLocked() {
"FmodAudioDevice string ABI contract validation failed");
}
if (capture_profile_supported_) {
for (std::size_t i = 0; i < kCaptureSlots.size(); ++i) {
if (!IsExecutableImageRange(library_base_, kCaptureRvas[i], 1) ||
vtable_[kCaptureSlots[i]] != library_base_ + kCaptureRvas[i])
return FailedPrecondition("native input capture vtable ABI mismatch");
original_capture_methods_[i] = vtable_[kCaptureSlots[i]];
}
if (!IsExecutableImageRange(library_base_, 0x3213154, 1) ||
!IsExecutableImageRange(library_base_, 0x1dc4cd0, 1) ||
!IsRelroImageRange(library_base_, 0x6cd4550, 24))
return FailedPrecondition("native input capture sink ABI outside image");
capture_ = std::make_unique<NativeInputCapture>(
NativeInputCapture::SinkAbi{
library_base_ + 0x6cd4550,
reinterpret_cast<NativeInputCapture::DeliverPcm>(
library_base_ + 0x3213154),
reinterpret_cast<NativeInputCapture::Destroy>(
library_base_ + 0x1dc4cd0),
reinterpret_cast<NativeInputCapture::OriginalLatency>(
library_base_ + kCaptureRvas[1])},
!input_devices_.empty());
}
original_methods_ = {
vtable_[kOutputCountVtableIndex],
vtable_[kOutputInfoVtableIndex],
@@ -478,6 +518,18 @@ Status RobloxOutputDeviceBridge::PatchVtableLocked() {
for (std::size_t i = 0; i < indexes.size(); ++i)
__atomic_store_n(&vtable_[indexes[i]], methods[i], __ATOMIC_RELEASE);
}
if (capture_) {
g_native_capture.store(capture_.get(), std::memory_order_release);
const std::array<std::uintptr_t, 6> methods{
FunctionAddress(&NativeInputCapture::Format),
FunctionAddress(&NativeInputCapture::Latency),
FunctionAddress(&NativeInputCapture::Start),
FunctionAddress(&NativeInputCapture::Stop),
FunctionAddress(&NativeInputCapture::Poll),
FunctionAddress(&NativeInputCapture::Recording)};
for (std::size_t i = 0; i < kCaptureSlots.size(); ++i)
__atomic_store_n(&vtable_[kCaptureSlots[i]], methods[i], __ATOMIC_RELEASE);
}
if (!SetVtableWritable(vtable_, kSelectOutputVtableIndex, false)) {
__atomic_store_n(&vtable_[kOutputCountVtableIndex], original_methods_[0],
__ATOMIC_RELEASE);
@@ -493,6 +545,13 @@ Status RobloxOutputDeviceBridge::PatchVtableLocked() {
__atomic_store_n(&vtable_[indexes[i]], original_methods_[4 + i],
__ATOMIC_RELEASE);
}
if (capture_) {
for (std::size_t i = 0; i < kCaptureSlots.size(); ++i)
__atomic_store_n(&vtable_[kCaptureSlots[i]], original_capture_methods_[i],
__ATOMIC_RELEASE);
g_native_capture.store(nullptr, std::memory_order_release);
capture_.reset();
}
(void)SetVtableWritable(vtable_, kSelectOutputVtableIndex, false);
return Status::Error(StatusCode::kPlatformError,
"cannot restore FmodAudioDevice RELRO protection");
@@ -521,6 +580,11 @@ bool RobloxOutputDeviceBridge::RestoreVtableLocked() {
__atomic_store_n(&vtable_[indexes[i]], original_methods_[4 + i],
__ATOMIC_RELEASE);
}
if (capture_) {
for (std::size_t i = 0; i < kCaptureSlots.size(); ++i)
__atomic_store_n(&vtable_[kCaptureSlots[i]], original_capture_methods_[i],
__ATOMIC_RELEASE);
}
return SetVtableWritable(vtable_, kSelectOutputVtableIndex, false);
}
+30 -5
View File
@@ -30,6 +30,9 @@ struct CaptureSession {
void* data_context = nullptr;
std::unique_ptr<AudioCapture> capture;
std::shared_ptr<std::atomic<bool>> microphone_muted;
std::atomic<std::uint64_t> delivered_buffers{0};
std::atomic<std::uint64_t> audible_buffers{0};
std::atomic<std::uint64_t> muted_buffers{0};
};
struct PlayoutSession {
@@ -112,22 +115,40 @@ void DisposeManager(void* opaque_context, const void* identity) {
void SetMicrophoneMute(void* opaque_context, bool muted) {
auto* context = static_cast<WebRtcJniAudioBridgeContext*>(opaque_context);
if (context == nullptr) return;
context->microphone_muted->store(muted, std::memory_order_release);
if (context->microphone_muted->exchange(muted, std::memory_order_acq_rel) != muted) {
std::cerr << " [mocktail][audio] WebRTC microphone muted=" << muted << '\n';
}
}
void OnCapturedAudio(void* opaque_session, std::size_t size_bytes) {
auto* session = static_cast<CaptureSession*>(opaque_session);
if (session != nullptr && session->data_callback != nullptr) {
if (session->capture == nullptr ||
size_bytes > session->capture->buffer_size_bytes()) {
return;
}
const auto* samples = static_cast<const unsigned char*>(
session->capture->buffer_data());
int peak = 0;
for (std::size_t i = 0; i + 1 < size_bytes; i += 2) {
const int word = samples[i] | (static_cast<int>(samples[i + 1]) << 8);
peak = std::max(peak, std::abs(word >= 32768 ? word - 65536 : word));
}
if (peak != 0 && session->audible_buffers.fetch_add(1) == 0) {
std::cerr << " [mocktail][audio] WebRTC microphone first nonzero PCM"
<< " peak=" << peak << "/32768\n";
}
if (session->microphone_muted != nullptr &&
session->microphone_muted->load(std::memory_order_acquire)) {
if (session->capture == nullptr ||
size_bytes > session->capture->buffer_size_bytes()) {
return;
}
session->muted_buffers.fetch_add(1, std::memory_order_relaxed);
std::memset(session->capture->buffer_data(), 0, size_bytes);
}
session->data_callback(session->data_context, session->identity,
size_bytes);
if (session->delivered_buffers.fetch_add(1) == 0) {
std::cerr << " [mocktail][audio] WebRTC microphone first JNI delivery"
<< " bytes=" << size_bytes << '\n';
}
}
}
@@ -137,6 +158,10 @@ void CloseCaptureSession(const std::shared_ptr<CaptureSession>& session) {
}
(void)session->capture->Stop();
session->capture->Shutdown();
std::cerr << " [mocktail][audio] WebRTC microphone capture summary"
<< " buffers=" << session->delivered_buffers.load()
<< " nonzero=" << session->audible_buffers.load()
<< " muted=" << session->muted_buffers.load() << '\n';
}
int Init(void* opaque_context, const void* identity, int sample_rate_hz,
+14 -1
View File
@@ -1,5 +1,7 @@
#include "jnivm/jnivm.h"
#include "mocktail/audio/fmod_thread_floating_point.h"
#include <algorithm>
#include <atomic>
#include <cctype>
@@ -56,6 +58,7 @@ namespace {
thread_local JNIEnv* g_thread_local_env = nullptr;
thread_local JNIEnv g_thread_env_storage = {};
thread_local VM* g_thread_vm_instance = nullptr;
thread_local mocktail::audio::FmodThreadFloatingPointMode g_thread_audio_fp_mode;
thread_local std::vector<std::vector<jobject>> g_local_frames;
std::recursive_mutex g_jni_state_mutex;
@@ -4970,6 +4973,7 @@ VM::~VM() {
g_live_vms.erase(std::remove(g_live_vms.begin(), g_live_vms.end(), this),
g_live_vms.end());
if (g_thread_vm_instance == this) {
g_thread_audio_fp_mode.Restore();
g_thread_vm_instance = nullptr;
g_thread_local_env = nullptr;
g_thread_env_storage.functions = nullptr;
@@ -6323,6 +6327,7 @@ JNIEnv* VM::GetJNIEnv() {
}
return g_thread_local_env;
}
g_thread_audio_fp_mode.Restore();
g_thread_vm_instance = this;
g_thread_env_storage.functions = &native_interface_;
g_thread_local_env = &g_thread_env_storage;
@@ -6372,7 +6377,7 @@ std::shared_ptr<Class> VM::FindClass(const std::string& class_name) const {
void VM::InitJNIFunctionTables() {
invoke_interface_.AttachCurrentThread =
[](JavaVM* vm, void** env, void* /*args*/) -> jint {
[](JavaVM* vm, void** env, void* args) -> jint {
if (JniVmTraceEnabled()) {
std::cout << " [JNI] AttachCurrentThread enter vm=" << vm
<< " env_out=" << env << '\n';
@@ -6389,6 +6394,7 @@ void VM::InitJNIFunctionTables() {
return JNI_EINVAL;
}
if (g_thread_vm_instance != owner || !IsThreadLocalEnvValid()) {
g_thread_audio_fp_mode.Restore();
g_thread_vm_instance = owner;
if (!owner->jni_env_) {
owner->jni_env_ = &owner->jni_env_storage_;
@@ -6398,6 +6404,12 @@ void VM::InitJNIFunctionTables() {
owner->jni_env_->functions ? owner->jni_env_->functions
: &owner->native_interface_;
g_thread_local_env = &g_thread_env_storage;
const auto* attach_args = static_cast<const JavaVMAttachArgs*>(args);
if (attach_args != nullptr &&
g_thread_audio_fp_mode.Enable(attach_args->name)) {
std::fprintf(stderr, " [mocktail][audio] %s: enabled FTZ/DAZ\n",
attach_args->name);
}
}
// Reattaching to the same VM must retain any guest JNI table wrapper.
*env = g_thread_local_env;
@@ -6421,6 +6433,7 @@ void VM::InitJNIFunctionTables() {
if (g_thread_vm_instance != owner || !IsThreadLocalEnvValid()) {
return JNI_EDETACHED;
}
g_thread_audio_fp_mode.Restore();
g_thread_local_env = nullptr;
g_thread_vm_instance = nullptr;
g_thread_env_storage.functions = nullptr;
+140
View File
@@ -13,6 +13,10 @@
#include <thread>
#include <vector>
#if defined(__x86_64__)
#include <xmmintrin.h>
#endif
namespace jnivm {
namespace {
@@ -376,6 +380,142 @@ TEST_F(JniVmTest, JniEnvPointerIsNotNull) {
EXPECT_NE(vm_->GetJNIEnv(), nullptr);
}
#if defined(__x86_64__)
constexpr unsigned int kAudioFpMode = (1U << 15) | (1U << 6);
class ScopedMxcsr final {
public:
ScopedMxcsr() : saved_(_mm_getcsr()) {}
~ScopedMxcsr() { _mm_setcsr(saved_); }
private:
unsigned int saved_;
};
// Volatile input ensures the multiply executes in SSE with the current MXCSR,
// rather than being constant-folded using the compiler's default FP model.
float MultiplyAudioSample(volatile float* sample, float gain) {
return _mm_cvtss_f32(_mm_mul_ss(_mm_set_ss(*sample), _mm_set_ss(gain)));
}
TEST_F(JniVmTest, FmodMixerFlushesSubnormalAudioWithoutChangingParentThread) {
ScopedMxcsr restore;
_mm_setcsr(0x1f80); // IEEE defaults; FTZ/DAZ disabled.
JavaVM* java_vm = vm_->GetJavaVM();
std::thread mixer([java_vm]() {
ScopedMxcsr restore_worker;
volatile float tail = 0x1p-140f;
volatile float quiet_normal = 0x1p-126f;
volatile float audible = 0.25f;
EXPECT_GT(MultiplyAudioSample(&tail, 1.0f), 0.0f);
EXPECT_GT(MultiplyAudioSample(&quiet_normal, 0.5f), 0.0f);
const unsigned int before = _mm_getcsr();
char name[] = "FMOD mixer thread";
JavaVMAttachArgs args{JNI_VERSION_1_6, name, nullptr};
void* env = nullptr;
ASSERT_EQ(java_vm->AttachCurrentThread(&env, &args), JNI_OK);
EXPECT_EQ(_mm_getcsr(), before | kAudioFpMode);
EXPECT_EQ(MultiplyAudioSample(&tail, 1.0f), 0.0f);
EXPECT_EQ(MultiplyAudioSample(&quiet_normal, 0.5f), 0.0f);
EXPECT_EQ(MultiplyAudioSample(&audible, 0.5f), 0.125f);
// Reattaching with no name must not lose the mixer's saved FP state.
ASSERT_EQ(java_vm->AttachCurrentThread(&env, nullptr), JNI_OK);
EXPECT_EQ(_mm_getcsr() & kAudioFpMode, kAudioFpMode);
ASSERT_EQ(java_vm->DetachCurrentThread(), JNI_OK);
EXPECT_EQ(_mm_getcsr() & kAudioFpMode, before & kAudioFpMode);
EXPECT_GT(MultiplyAudioSample(&tail, 1.0f), 0.0f);
});
mixer.join();
EXPECT_EQ(_mm_getcsr(), 0x1f80U);
}
TEST_F(JniVmTest, FmodDaemonThreadsRestoreExistingModesAndOtherControls) {
ScopedMxcsr restore;
JavaVM* java_vm = vm_->GetJavaVM();
for (const char* name : {"FMOD mixer thread", "FMOD feeder thread",
"FMOD Convolution thread", "FMOD Worker Thread"}) {
for (const unsigned int mode : {0U, 1U << 6, 1U << 15, kAudioFpMode}) {
_mm_setcsr(0x1f80 | mode | _MM_ROUND_DOWN);
const unsigned int before = _mm_getcsr();
JavaVMAttachArgs args{JNI_VERSION_1_6, const_cast<char*>(name), nullptr};
void* env = nullptr;
ASSERT_EQ(java_vm->AttachCurrentThreadAsDaemon(&env, &args), JNI_OK);
EXPECT_EQ(_mm_getcsr(), before | kAudioFpMode);
// Guest changes to other controls/status must survive detach.
_mm_setcsr((_mm_getcsr() & ~_MM_ROUND_MASK) | _MM_ROUND_UP | 0x20U);
const unsigned int after_guest = _mm_getcsr();
ASSERT_EQ(java_vm->DetachCurrentThread(), JNI_OK);
EXPECT_EQ(_mm_getcsr(), (after_guest & ~kAudioFpMode) | mode);
}
}
}
TEST_F(JniVmTest, OrdinaryJniThreadsKeepIeeeFloatingPointBehavior) {
ScopedMxcsr restore;
_mm_setcsr(0x1f80);
JavaVM* java_vm = vm_->GetJavaVM();
for (const char* name : {"Main", "RBX Worker", "FMOD mixer thread extra",
"FMOD file thread", "", static_cast<const char*>(nullptr)}) {
JavaVMAttachArgs args{JNI_VERSION_1_6, const_cast<char*>(name), nullptr};
void* env = nullptr;
ASSERT_EQ(java_vm->AttachCurrentThread(&env, &args), JNI_OK);
EXPECT_EQ(_mm_getcsr(), 0x1f80U);
// JNI ignores new names when a thread is already attached.
args.name = const_cast<char*>("FMOD mixer thread");
ASSERT_EQ(java_vm->AttachCurrentThread(&env, &args), JNI_OK);
EXPECT_EQ(_mm_getcsr(), 0x1f80U);
ASSERT_EQ(java_vm->DetachCurrentThread(), JNI_OK);
}
void* env = nullptr;
ASSERT_EQ(java_vm->AttachCurrentThread(&env, nullptr), JNI_OK);
EXPECT_EQ(_mm_getcsr(), 0x1f80U);
EXPECT_EQ(java_vm->DetachCurrentThread(), JNI_OK);
}
TEST_F(JniVmTest, RejectedFmodAttachDoesNotChangeFloatingPointMode) {
ScopedMxcsr restore;
_mm_setcsr(0x1f80);
JavaVM* java_vm = vm_->GetJavaVM();
char name[] = "FMOD mixer thread";
JavaVMAttachArgs args{JNI_VERSION_1_6, name, nullptr};
EXPECT_EQ(java_vm->AttachCurrentThread(nullptr, &args), JNI_EINVAL);
EXPECT_EQ(_mm_getcsr(), 0x1f80U);
JavaVM foreign = {java_vm->functions};
void* env = nullptr;
EXPECT_EQ(foreign.AttachCurrentThread(&env, &args), JNI_EINVAL);
EXPECT_EQ(_mm_getcsr(), 0x1f80U);
}
TEST_F(JniVmTest, ChangingVmRestoresFmodFloatingPointMode) {
ScopedMxcsr restore;
_mm_setcsr(0x1f80);
JavaVM* java_vm = vm_->GetJavaVM();
char name[] = "FMOD mixer thread";
JavaVMAttachArgs args{JNI_VERSION_1_6, name, nullptr};
void* env = nullptr;
ASSERT_EQ(java_vm->AttachCurrentThread(&env, &args), JNI_OK);
{
VM temporary;
// Merely constructing/destroying another VM must not change this thread.
}
EXPECT_EQ(_mm_getcsr() & kAudioFpMode, kAudioFpMode);
{
VM other;
ASSERT_EQ(other.GetJavaVM()->AttachCurrentThread(&env, nullptr), JNI_OK);
EXPECT_EQ(_mm_getcsr(), 0x1f80U);
}
ASSERT_EQ(java_vm->AttachCurrentThread(&env, &args), JNI_OK);
{
VM other;
ASSERT_NE(other.GetJNIEnv(), nullptr);
EXPECT_EQ(_mm_getcsr(), 0x1f80U);
}
ASSERT_EQ(java_vm->AttachCurrentThread(&env, &args), JNI_OK);
vm_.reset();
EXPECT_EQ(_mm_getcsr(), 0x1f80U);
}
#endif
TEST_F(JniVmTest, DiscardedTemporaryVmKeepsOriginalUsable) {
JavaVM* java_vm = vm_->GetJavaVM();
JNIEnv* original_env = vm_->GetJNIEnv();