mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-24 18:00:59 +02:00
rewrite some text
This commit is contained in:
1 parent
88b7382577
commit
5d636749fa
74 files changed
+672
-1420
No files matched your search
@@ -134,7 +134,7 @@ run-angle: ## Run with ANGLE/Vulkan compatibility backend
|
||||
update-roblox: ## Validate and import the current x86_64 Roblox bundle cached by Sober
|
||||
@./scripts/update_roblox_payload.sh
|
||||
|
||||
update-auto: build ## Download latest Roblox, derive its ABI, canary twice and activate safely
|
||||
update-auto: build ## Download latest Roblox, derive its ABI, run two canaries, then activate
|
||||
@./$(BUILD_DIR)/mocktail_updater update
|
||||
|
||||
update-auto-launch: build ## Update Roblox and launch the verified current payload
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
[](LICENSE)
|
||||
|
||||
Mocktail runs the Android `x86_64` Roblox client on Linux. It provides the
|
||||
Android ABI and JNI pieces the client expects, then uses SDL3 and Vulkan or
|
||||
OpenGL for the Linux side.
|
||||
Android ABI and JNI pieces the client expects, then connects them to SDL3 and
|
||||
Vulkan or OpenGL on the Linux side.
|
||||
|
||||
Mocktail is still experimental and may break after a Roblox update. It is not
|
||||
affiliated with Roblox Corporation or VinegarHQ.
|
||||
Mocktail is an independent community project. It is not affiliated with Roblox
|
||||
Corporation or VinegarHQ and does not distribute the Roblox client.
|
||||
|
||||
## How it works
|
||||
|
||||
|
||||
@@ -19,16 +19,8 @@ namespace mocktail::compat {
|
||||
|
||||
using BionicAtForkCallback = void (*)(void);
|
||||
|
||||
// Registers Android's __register_atfork callback contract with the host
|
||||
// pthread runtime. Bionic associates registrations with dso_handle and
|
||||
// removes them when that DSO unloads. POSIX pthread_atfork has no matching
|
||||
// unregister operation, so the host bridge accepts dso_handle for ABI
|
||||
// compatibility but cannot use it for cleanup.
|
||||
//
|
||||
// Callback code must remain mapped for every later fork. In practice, a guest
|
||||
// DSO that has registered callbacks must stay loaded until process exit;
|
||||
// unloading is safe only when the process guarantees that it will never fork
|
||||
// again.
|
||||
// POSIX cannot unregister by dso_handle, so registered guest callbacks must
|
||||
// remain mapped for every later fork.
|
||||
int RegisterBionicAtFork(BionicAtForkCallback prepare,
|
||||
BionicAtForkCallback parent,
|
||||
BionicAtForkCallback child, void* dso_handle) noexcept;
|
||||
@@ -37,8 +29,6 @@ int RegisterBionicAtFork(BionicAtForkCallback prepare,
|
||||
|
||||
extern "C" {
|
||||
|
||||
// Exact LP64 Android/Bionic __register_atfork signature. The prefixed host
|
||||
// symbol is exported to the guest as "__register_atfork" by synthetic libc.so.
|
||||
int mocktail_bionic_register_atfork(
|
||||
mocktail::compat::BionicAtForkCallback prepare,
|
||||
mocktail::compat::BionicAtForkCallback parent,
|
||||
|
||||
@@ -22,11 +22,8 @@
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
// Android x86-64's malloc.h declares ten size_t fields in this order. This is
|
||||
// not the host's struct mallinfo: glibc's legacy type uses int,
|
||||
// and musl exposes no allocator-telemetry API. On hosts without a compatible
|
||||
// size_t-based telemetry API, BionicMallinfo returns an all-zero "unknown"
|
||||
// snapshot. Callers must not interpret that fallback as proof of zero usage.
|
||||
// Android uses size_t fields here; glibc uses int and musl has no equivalent.
|
||||
// An all-zero fallback means unknown, not zero usage.
|
||||
struct BionicMallinfoSnapshot {
|
||||
size_t arena = 0;
|
||||
size_t ordblks = 0;
|
||||
@@ -44,10 +41,7 @@ struct BionicLocaleState;
|
||||
using BionicLocale = BionicLocaleState*;
|
||||
using BionicThreadDestructor = void (*)(void*);
|
||||
|
||||
// Do not use the host socket structs here. Although glibc happens to match
|
||||
// Android LP64, musl declares cmsg_len and msg_controllen as 32-bit socklen_t.
|
||||
// Bionic x86-64 uses size_t for both, as audited from the vendored
|
||||
// bionic/libc/include/sys/socket.h.
|
||||
// Bionic uses size_t where musl uses 32-bit socklen_t.
|
||||
struct BionicMessageHeader {
|
||||
void* msg_name = nullptr;
|
||||
uint32_t msg_namelen = 0;
|
||||
@@ -64,35 +58,24 @@ struct BionicControlMessageHeader {
|
||||
int cmsg_type = 0;
|
||||
};
|
||||
|
||||
// Implements Android's exported CMSG_NXTHDR helper over the explicit Bionic
|
||||
// x86-64 msghdr/cmsghdr layout above. Malformed or truncated control buffers
|
||||
// fail closed with nullptr.
|
||||
// Malformed or truncated control buffers return nullptr.
|
||||
BionicControlMessageHeader* BionicCmsgNextHeader(
|
||||
BionicMessageHeader* message, BionicControlMessageHeader* current) noexcept;
|
||||
|
||||
// Registers one destructor for the current thread using libstdc++'s Itanium
|
||||
// C++ ABI implementation. That implementation is present on both supported
|
||||
// glibc and musl toolchains and preserves destructor execution at thread exit.
|
||||
// Uses libstdc++'s Itanium ABI on both supported libc implementations.
|
||||
int BionicCxaThreadAtExit(BionicThreadDestructor destructor, void* argument,
|
||||
void* dso_handle) noexcept;
|
||||
|
||||
// Fills the complete buffer from the Linux kernel RNG. The implementation
|
||||
// falls back to /dev/urandom only when getrandom is unavailable or blocked and
|
||||
// aborts rather than returning predictable or partially initialized bytes.
|
||||
// Falls back to /dev/urandom; never returns partial or predictable bytes.
|
||||
void BionicArc4RandomBuffer(void* buffer, size_t size) noexcept;
|
||||
|
||||
BionicMallinfoSnapshot BionicMallinfo() noexcept;
|
||||
bool BionicMallinfoHasHostTelemetry() noexcept;
|
||||
|
||||
// Formats one Linux errno value using Bionic's POSIX strerror_r contract.
|
||||
// Unlike glibc's GNU variant, success is reported as zero; truncation returns
|
||||
// -1 and sets errno to ERANGE while preserving the caller's errno on success.
|
||||
// POSIX strerror_r contract: zero on success, -1/ERANGE on truncation.
|
||||
int BionicStrError(int error_number, char* buffer, size_t buffer_size) noexcept;
|
||||
|
||||
// Bionic supports only C and C.UTF-8, whose integer parsing is identical. A
|
||||
// host C locale is installed only for the duration of each call, so ambient
|
||||
// process locale cannot change guest parsing and a Bionic locale pointer is
|
||||
// never mistaken for an incompatible host locale_t.
|
||||
// Parses under host C locale without passing an incompatible Bionic locale_t.
|
||||
long long BionicStrToLongLongLocale(const char* text, char** end, int base,
|
||||
BionicLocale locale) noexcept;
|
||||
unsigned long long BionicStrToUnsignedLongLongLocale(
|
||||
|
||||
@@ -23,30 +23,21 @@ namespace mocktail::compat {
|
||||
|
||||
using NativeThreadInitializer = void (*)();
|
||||
|
||||
// Pinned Bionic LP64 pthread_internal.h keeps total per-thread reservation
|
||||
// near 1 MiB by subtracting its 32 KiB alternate signal stack from the normal
|
||||
// thread stack. Host defaults are not ABI substitutes: musl's default is only
|
||||
// 128 KiB on the supported Void runtime.
|
||||
// Bionic reserves 32 KiB of its 1 MiB thread allocation for the signal stack.
|
||||
inline constexpr size_t kBionicLp64DefaultThreadStackSize =
|
||||
(1U * 1024U * 1024U) - (32U * 1024U);
|
||||
static_assert(kBionicLp64DefaultThreadStackSize == 1015808U);
|
||||
|
||||
// Used only after a Bionic-sized stack is rejected by the host at thread
|
||||
// creation time. The retry takes the largest of this floor, the requested
|
||||
// guest size, and the host default. That retains enough stack on musl without
|
||||
// shrinking glibc's default.
|
||||
// Retry floor when the host rejects the Bionic-sized stack.
|
||||
inline constexpr size_t kBionicLp64FallbackThreadStackSize = 2U * 1024U * 1024U;
|
||||
|
||||
// Installs the payload-owned per-thread initializer used by subsequently
|
||||
// created guest threads. Passing nullptr restores ordinary host behavior.
|
||||
// nullptr restores host behavior.
|
||||
void ConfigureBionicPthreadThreadInitializer(
|
||||
NativeThreadInitializer initializer) noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
// Creates a host thread after rebuilding the supported POSIX attributes.
|
||||
// Rebuilding avoids passing ABI-private pthread_attr_t state from the Android
|
||||
// payload directly into glibc.
|
||||
// Rebuilds attributes instead of passing Android pthread_attr_t to glibc.
|
||||
extern "C" int mocktail_bionic_pthread_create(pthread_t* thread,
|
||||
const pthread_attr_t* attr,
|
||||
void* (*start_routine)(void*),
|
||||
|
||||
@@ -31,9 +31,8 @@ constexpr uint32_t kBionicPthreadKeyValidFlag = uint32_t{1} << 31;
|
||||
|
||||
using BionicPthreadKeyDestructor = void (*)(void*);
|
||||
|
||||
// implementation of Bionic's pthread_key_* ABI. It owns only guest
|
||||
// keys registered through the synthetic Android libc.so, while ordinary host
|
||||
// code continues to use glibc pthread keys.
|
||||
// Implements Bionic's pthread_key_* ABI for keys registered through the
|
||||
// synthetic libc.so. Host code continues to use glibc pthread keys.
|
||||
class BionicPthreadKeyRuntime {
|
||||
public:
|
||||
static BionicPthreadKeyRuntime& Instance() noexcept;
|
||||
|
||||
@@ -62,7 +62,7 @@ enum class NativePreJniBootstrapStatus {
|
||||
kFailed,
|
||||
};
|
||||
|
||||
// Installs one exact-Build-ID experiment before libroblox constructors run.
|
||||
// Installs one exact-Build-ID profile before libroblox constructors run.
|
||||
// The caller owns policy: this function never enables itself from environment
|
||||
// variables and must only receive a profile selected from the loaded ELF ID.
|
||||
HostAbiExperimentResult
|
||||
|
||||
@@ -81,7 +81,7 @@ struct NativeAllocatorProfile {
|
||||
inline constexpr size_t kMaxHostBridgeEntries = 8;
|
||||
inline constexpr size_t kMaxConstructorRunRanges = 26;
|
||||
|
||||
// Build-ID-scoped native ABI knowledge for supported and experimental payload
|
||||
// Build-ID-scoped native ABI knowledge for supported and candidate payload
|
||||
// profiles. Keeping these addresses outside the legacy startup graph makes the
|
||||
// version boundary reviewable and independently testable.
|
||||
struct HostAbiProfile {
|
||||
|
||||
@@ -28,11 +28,8 @@ struct FileSha256Result {
|
||||
explicit operator bool() const noexcept { return error.empty(); }
|
||||
};
|
||||
|
||||
// Computes a lowercase SHA-256 digest without invoking a shell or an external
|
||||
// utility. The external ABI approval receipt binds exact profile,
|
||||
// compatibility-manifest, and payload bytes through this implementation; the
|
||||
// runtime itself is bound by its stable GNU Build ID because installation may
|
||||
// rewrite RPATH bytes.
|
||||
// Approval receipts bind the profile, manifest, and payload to this digest.
|
||||
// Runtime identity uses its Build ID because installation may rewrite RPATH.
|
||||
FileSha256Result ComputeFileSha256(const std::string& path);
|
||||
|
||||
struct ExternalHostAbiProfileRequest {
|
||||
@@ -55,27 +52,19 @@ struct ExternalHostAbiProfileResult {
|
||||
explicit operator bool() const noexcept { return error.empty(); }
|
||||
};
|
||||
|
||||
// Loads one exact, payload-bound ABI profile into the process registry. A
|
||||
// candidate is accepted only by an explicitly authorized canary process. A
|
||||
// normal launch instead requires a separate approval receipt produced after
|
||||
// repeated Tier C readiness. Built-in profiles are immutable and always win.
|
||||
// Candidates require canary authorization; normal launches require an
|
||||
// approval receipt. Built-in profiles always win.
|
||||
ExternalHostAbiProfileResult LoadExternalHostAbiProfile(
|
||||
const ExternalHostAbiProfileRequest& request);
|
||||
|
||||
// Environment integration used by the pre-native compatibility gate. An
|
||||
// unset MOCKTAIL_HOST_ABI_PROFILE_FILE is a successful no-op. If it is set,
|
||||
// candidate authorization requires MOCKTAIL_HOST_ABI_CANARY=1,
|
||||
// MOCKTAIL_ALLOW_CANDIDATE_HOST_ABI=1, and the explicit research command-line
|
||||
// marker. Approved profiles require MOCKTAIL_HOST_ABI_APPROVAL_RECEIPT and
|
||||
// neither canary environment marker.
|
||||
// An unset profile is a no-op. Candidates require both canary flags and the
|
||||
// research CLI marker; approved profiles require a receipt and no canary flag.
|
||||
ExternalHostAbiProfileResult InitializeHostAbiProfileFromEnvironment(
|
||||
const std::string& payload_path,
|
||||
const std::string& compatibility_manifest_path,
|
||||
const std::string& expected_build_id,
|
||||
bool explicit_unverified_authorization);
|
||||
|
||||
// Registry lookup used by FindHostAbiProfile after its immutable built-in
|
||||
// table. Callers should use FindHostAbiProfile rather than this function.
|
||||
const HostAbiProfile* FindLoadedExternalHostAbiProfile(
|
||||
std::string_view build_id) noexcept;
|
||||
|
||||
|
||||
+15
-98
@@ -12,20 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// jnivm/jnivm.h — Pseudo Java Native Interface Virtual Machine.
|
||||
//
|
||||
// Provides a lightweight C++ implementation of a JVM stub for use with
|
||||
// Android-native shared libraries (e.g., libroblox.so) running on Linux.
|
||||
// No real Java bytecode is executed; instead, all JNI calls are intercepted
|
||||
// and dispatched to registered C++ callbacks.
|
||||
//
|
||||
// Usage:
|
||||
// auto vm = std::make_shared<jnivm::VM>();
|
||||
// auto cls = vm->RegisterClass("android/content/Context");
|
||||
// cls->RegisterMethod("getSystemService",
|
||||
// "(Ljava/lang/String;)Ljava/lang/Object;",
|
||||
// [](JNIEnv*, jobject, ...) { return nullptr; });
|
||||
|
||||
#ifndef SOBER_TEST_JNIVM_JNIVM_H_
|
||||
#define SOBER_TEST_JNIVM_JNIVM_H_
|
||||
|
||||
@@ -42,29 +28,20 @@
|
||||
|
||||
namespace jnivm {
|
||||
|
||||
// Forward declarations
|
||||
|
||||
class VM;
|
||||
class Class;
|
||||
|
||||
// Object — base type for all pseudo-Java objects managed by the pseudo-JVM.
|
||||
// Analogous to java.lang.Object in the real JVM.
|
||||
|
||||
class Object {
|
||||
public:
|
||||
explicit Object(std::shared_ptr<Class> klass) : klass_(std::move(klass)) {}
|
||||
virtual ~Object() = default;
|
||||
|
||||
// Returns the pseudo-class descriptor of this object.
|
||||
std::shared_ptr<Class> GetClass() const { return klass_; }
|
||||
|
||||
private:
|
||||
std::shared_ptr<Class> klass_;
|
||||
};
|
||||
|
||||
// MethodCallback — signature for registered JNI method stubs.
|
||||
// The callback receives the raw JNIEnv pointer and the receiver jobject.
|
||||
|
||||
using MethodCallback = std::function<void(JNIEnv *, jobject)>;
|
||||
|
||||
// Resolved Roblox account identity supplied by the host runtime. This
|
||||
@@ -75,8 +52,6 @@ struct RobloxAuthIdentity {
|
||||
std::string display_name;
|
||||
};
|
||||
|
||||
// Host device capabilities exposed through the pseudo Android framework.
|
||||
// The guest ABI remains Android, while the default profile // describes a Linux desktop with physical mouse and keyboard input.
|
||||
struct PlatformIdentity {
|
||||
bool touch_enabled = false;
|
||||
bool mouse_enabled = true;
|
||||
@@ -99,23 +74,16 @@ struct RobloxCredentialView {
|
||||
|
||||
using RobloxCredentialProvider = RobloxCredentialView (*)(const void *context);
|
||||
|
||||
// Creates a Configuration object consistent with the VM's immutable platform
|
||||
// identity snapshot. This is the replacement for legacy construction of
|
||||
// contradictory Android keyboard/touch fields.
|
||||
jobject CreateAndroidConfiguration(JNIEnv *env);
|
||||
|
||||
// Narrow write-only boundary for credentials produced by Roblox after the
|
||||
// native runtime has started. A successfully persisted value supersedes the
|
||||
// bound provider for the remainder of the current VM lifetime so native sign-in
|
||||
// can become authoritative without reopening an untrusted credential source.
|
||||
// A stored credential replaces the bound provider for the rest of this VM's
|
||||
// lifetime.
|
||||
struct RobloxCredentialSinkCallbacks {
|
||||
bool (*store)(void *context, const char *data, std::size_t size) = nullptr;
|
||||
};
|
||||
|
||||
// Narrow host callback boundary for org/fmod/AudioDevice. The pseudo-JVM owns
|
||||
// only an opaque shared context and never depends on the SDL audio runtime.
|
||||
// Each Java receiver is passed as a stable identity so the host can keep
|
||||
// independent device lifecycles without retaining a JNI local reference.
|
||||
// The VM keeps the host context opaque. Receiver identities let the backend
|
||||
// track devices without retaining JNI local references.
|
||||
struct FmodAudioDeviceCallbacks {
|
||||
bool (*init)(void *context, const void *identity, int channels,
|
||||
int sample_rate_hz, int block_size_frames,
|
||||
@@ -126,17 +94,12 @@ struct FmodAudioDeviceCallbacks {
|
||||
void (*shutdown)(void *context) = nullptr;
|
||||
};
|
||||
|
||||
// Narrow host callback boundary for the activity.setWindowFlags(II)V contract.
|
||||
// Production LuaApp retains its activity through the android.app.Activity
|
||||
// interface, while GameActivity startup uses the concrete subclass. Guest JNI
|
||||
// threads only publish Android flags; the platform callback must marshal any
|
||||
// SDL work onto the SDL main thread.
|
||||
// Guest JNI threads only publish Android window flags. The host callback must
|
||||
// marshal SDL work onto the main thread.
|
||||
struct AndroidWindowCallbacks {
|
||||
bool (*set_flags)(void *context, int flags, int mask) = nullptr;
|
||||
};
|
||||
|
||||
// Immutable snapshot of the Android NativeTextBoxInfo payload supplied by
|
||||
// Roblox when a native text field receives focus.
|
||||
struct RobloxTextBoxInfo {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
@@ -161,8 +124,7 @@ struct RobloxTextInputShowRequest {
|
||||
RobloxTextBoxInfo info;
|
||||
};
|
||||
|
||||
// Narrow callback boundary for the Roblox native text-input callbacks. The
|
||||
// pseudo-JVM snapshots all JNI-owned arguments before entering this boundary.
|
||||
// JNI-owned arguments are copied before the host callback runs.
|
||||
struct RobloxTextInputCallbacks {
|
||||
void (*show)(void *context,
|
||||
const RobloxTextInputShowRequest &request) = nullptr;
|
||||
@@ -172,9 +134,8 @@ struct RobloxTextInputCallbacks {
|
||||
void (*shutdown)(void *context) = nullptr;
|
||||
};
|
||||
|
||||
// Narrow boundary for a native MessageBus RawCallback object. The VM
|
||||
// retains context while a callback is in flight and dispatches only the exact
|
||||
// run(Ljava/lang/String;)V method on the exact created receiver.
|
||||
// Dispatch accepts run(String) only on a receiver created by this VM. The
|
||||
// context stays alive while the callback runs.
|
||||
struct MessageBusRawCallbacks {
|
||||
void (*run)(void *context, JNIEnv *env, jstring message) = nullptr;
|
||||
};
|
||||
@@ -183,18 +144,14 @@ struct MessageBusRequestHandlerCallbacks {
|
||||
std::string (*run)(void *context, JNIEnv *env, jstring message) = nullptr;
|
||||
};
|
||||
|
||||
// Narrow boundary for the current APK's legacy BrowserService transport.
|
||||
// Dispatch accepts only Callback.onItemSet(Ljava/lang/String;)V on the exact
|
||||
// pseudo receiver created by CreateMemStorageCallback.
|
||||
// Dispatch accepts Callback.onItemSet(String) only on a receiver created by
|
||||
// this VM. The context stays alive while the callback runs.
|
||||
struct MemStorageCallbackCallbacks {
|
||||
void (*on_item_set)(void *context, JNIEnv *env, jstring value) = nullptr;
|
||||
};
|
||||
|
||||
// Narrow boundary for the exact APK callbacks that update host web
|
||||
// state: NativeGLJavaInterface notifications, web activities, cookie sync,
|
||||
// and JNICookieProtocol.OnSetCookieHandler. The VM retains the shared callback
|
||||
// context while a callback is in flight; consumers must copy JNI strings
|
||||
// before returning.
|
||||
// Web callbacks share a retained context while in flight. Consumers must copy
|
||||
// JNI strings before returning.
|
||||
struct RobloxDataModelNotificationCallbacks {
|
||||
void (*on_notification)(void *context, JNIEnv *env, jstring type,
|
||||
jstring data) = nullptr;
|
||||
@@ -209,37 +166,22 @@ struct RobloxDataModelNotificationCallbacks {
|
||||
jstring url) = nullptr;
|
||||
};
|
||||
|
||||
// Narrow boundary for the exact NativeHelper lifecycle notification
|
||||
// emitted when Roblox returns from an experience to LuaApp.
|
||||
struct RobloxExperienceLifecycleCallbacks {
|
||||
void (*on_lua_app_did_return)(void *context) = nullptr;
|
||||
};
|
||||
|
||||
// Class — represents a pseudo Java class registered in the pseudo-JVM.
|
||||
// Stores the class name (JNI descriptor format) and a map of method stubs.
|
||||
|
||||
class Class {
|
||||
public:
|
||||
explicit Class(std::string name) : name_(std::move(name)) {}
|
||||
|
||||
// Returns the JNI-style class descriptor, e.g. "android/content/Context".
|
||||
const std::string &GetName() const { return name_; }
|
||||
|
||||
// Registers a JNI method stub.
|
||||
//
|
||||
// Args:
|
||||
// method_name: Simple method name (e.g. "getSystemService").
|
||||
// signature: JNI type descriptor (e.g. "(Ljava/lang/String;)V").
|
||||
// callback: C++ lambda invoked when the method is called via JNI.
|
||||
void RegisterMethod(const std::string &method_name,
|
||||
const std::string &signature, MethodCallback callback);
|
||||
|
||||
// Looks up a method stub by name and signature.
|
||||
// Returns nullptr if not found.
|
||||
const MethodCallback *FindMethod(const std::string &method_name,
|
||||
const std::string &signature) const;
|
||||
|
||||
// Returns all registered methods (name → callback).
|
||||
const std::unordered_map<std::string, MethodCallback> &GetMethods() const {
|
||||
return methods_;
|
||||
}
|
||||
@@ -247,57 +189,37 @@ public:
|
||||
private:
|
||||
std::string name_;
|
||||
|
||||
// Key format: "methodName:descriptor"
|
||||
std::unordered_map<std::string, MethodCallback> methods_;
|
||||
};
|
||||
|
||||
// VM — the top-level pseudo Java Virtual Machine.
|
||||
//
|
||||
// Owns a registry of pseudo-classes and exposes a JavaVM* compatible
|
||||
// pointer for passing to JNI_OnLoad in the loaded shared library.
|
||||
|
||||
class VM {
|
||||
public:
|
||||
VM();
|
||||
~VM();
|
||||
|
||||
// Disallow copy and assign.
|
||||
VM(const VM &) = delete;
|
||||
VM &operator=(const VM &) = delete;
|
||||
|
||||
// Registers a new pseudo-class by its JNI descriptor.
|
||||
// Repeated registration returns the existing instance.
|
||||
//
|
||||
// Args:
|
||||
// class_name: JNI-style descriptor, e.g.
|
||||
// "com/roblox/client/RobloxActivity".
|
||||
//
|
||||
// Returns:
|
||||
// Shared pointer to the registered (or pre-existing) Class object.
|
||||
// Repeated registration returns the existing class.
|
||||
std::shared_ptr<Class> RegisterClass(const std::string &class_name);
|
||||
|
||||
// Finds a previously registered class.
|
||||
// Returns nullptr if the class has not been registered.
|
||||
std::shared_ptr<Class> FindClass(const std::string &class_name) const;
|
||||
|
||||
// Returns the raw JavaVM pointer compatible with JNI_OnLoad signature.
|
||||
JavaVM *GetJavaVM() { return java_vm_; }
|
||||
|
||||
// Resolves the process pseudo-VM that owns an exact JavaVM pointer. Returns
|
||||
// null for foreign VMs and after the owner is destroyed.
|
||||
static VM *FromJavaVM(JavaVM *java_vm);
|
||||
|
||||
// Returns the attached JNIEnv pointer for the current host thread.
|
||||
JNIEnv *GetJNIEnv();
|
||||
|
||||
// Returns the number of registered classes.
|
||||
std::size_t GetClassCount() const { return class_registry_.size(); }
|
||||
|
||||
// Replaces the resolved account identity with an independent copy. Invalid
|
||||
// (non-positive) identities are normalized to the unresolved state.
|
||||
void SetRobloxAuthIdentity(const RobloxAuthIdentity &identity);
|
||||
|
||||
// Returns the VM to its unresolved account state.
|
||||
void ClearRobloxAuthIdentity();
|
||||
|
||||
// Returns an immutable-by-value snapshot safe for use by JNI callbacks on
|
||||
@@ -405,12 +327,10 @@ public:
|
||||
void ClearRobloxExperienceLifecycleCallbacks();
|
||||
bool DispatchRobloxExperienceLuaAppDidReturn();
|
||||
|
||||
// Restores env->functions to &native_interface_ in case libroblox replaced
|
||||
// it.
|
||||
// Restores the pseudo-JNI table after guest code replaces env->functions.
|
||||
void RestoreFunctions();
|
||||
|
||||
private:
|
||||
// Internal JNI function tables populated with stub implementations.
|
||||
JNIInvokeInterface_ invoke_interface_ = {};
|
||||
JavaVM java_vm_storage_ = {};
|
||||
JavaVM *java_vm_ = nullptr;
|
||||
@@ -419,7 +339,6 @@ private:
|
||||
JNIEnv jni_env_storage_ = {};
|
||||
JNIEnv *jni_env_ = nullptr;
|
||||
|
||||
// Registry of pseudo-classes indexed by their JNI descriptor.
|
||||
std::unordered_map<std::string, std::shared_ptr<Class>> class_registry_;
|
||||
|
||||
mutable std::mutex roblox_auth_identity_mutex_;
|
||||
@@ -498,8 +417,6 @@ private:
|
||||
std::shared_ptr<RobloxExperienceLifecycleBinding>
|
||||
roblox_experience_lifecycle_binding_;
|
||||
|
||||
// Initialises the JNIInvokeInterface_ and JNINativeInterface_ function tables
|
||||
// with stub implementations that forward calls to this VM instance.
|
||||
void InitJNIFunctionTables();
|
||||
};
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ class RuntimeDependencies final {
|
||||
ShutdownBeforePlatformCallback shutdown_before_platform_ = nullptr;
|
||||
};
|
||||
|
||||
// Transitional reverse-engineering runtime. New subsystems must expose // interfaces and be composed outside this implementation.
|
||||
// Transitional runtime kept for the reverse-engineered startup path.
|
||||
int Run(const runtime::CommandLineOptions& options,
|
||||
RuntimeDependencies dependencies);
|
||||
|
||||
|
||||
@@ -12,17 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// libc_shim/libc_shim.h — Bionic libc → Glibc call translation layer.
|
||||
//
|
||||
// Intercepts calls made by Bionic-compiled code (libroblox.so) to
|
||||
// Bionic-specific libc functions and forwards them to their Glibc equivalents
|
||||
// on the host Linux system.
|
||||
//
|
||||
// Key responsibilities:
|
||||
// - pthread_mutex_t size/layout differences between Bionic and Glibc.
|
||||
// - Virtual filesystem path mapping (APK sandbox → Flatpak paths).
|
||||
// - Bionic-only syscall wrappers missing in Glibc.
|
||||
|
||||
#ifndef SOBER_TEST_LIBC_SHIM_LIBC_SHIM_H_
|
||||
#define SOBER_TEST_LIBC_SHIM_LIBC_SHIM_H_
|
||||
|
||||
@@ -57,10 +46,8 @@ struct HostCaBundleResolution {
|
||||
// boundary as the caller that will later free them.
|
||||
void ConfigureGuestAllocator(GuestAllocator allocator) noexcept;
|
||||
|
||||
// Installs all shim hooks. Must be called before the target .so is loaded.
|
||||
//
|
||||
// After this call, dlopen(3) symbol resolution for known Bionic symbols will
|
||||
// be intercepted and redirected to the shim implementations.
|
||||
// Must run before loading the target library so its Bionic imports resolve to
|
||||
// these shims.
|
||||
void Install();
|
||||
|
||||
// Resolves the host TLS trust bundle without weakening certificate
|
||||
@@ -76,29 +63,13 @@ HostCaBundleResolution ConfigureHostCaBundlePathMappings();
|
||||
|
||||
const char* HostCaBundleStatusName(HostCaBundleStatus status);
|
||||
|
||||
// Maps an Android-style virtual path to its Flatpak sandbox equivalent.
|
||||
//
|
||||
// Example:
|
||||
// "/data/user/0/com.roblox.client/files/logs" →
|
||||
// "/home/user/.var/app/org.vinegarhq.Sober/data/sober/logs"
|
||||
//
|
||||
// Args:
|
||||
// android_path: Absolute path as seen by the Bionic library.
|
||||
//
|
||||
// Returns:
|
||||
// Translated host path. Returns android_path unchanged if no mapping
|
||||
// is registered for it.
|
||||
// Applies the longest matching Android-to-host path prefix. Unmapped paths are
|
||||
// returned unchanged.
|
||||
std::string TranslatePath(const std::string& android_path);
|
||||
|
||||
// Registers a path prefix mapping used by TranslatePath.
|
||||
//
|
||||
// Args:
|
||||
// android_prefix: Android path prefix to intercept.
|
||||
// host_prefix: Replacement prefix on the host filesystem.
|
||||
void RegisterPathMapping(const std::string& android_prefix,
|
||||
const std::string& host_prefix);
|
||||
|
||||
// Removes all registered path mappings. Primarily for use in tests.
|
||||
void ClearPathMappings();
|
||||
|
||||
} // namespace libc_shim
|
||||
|
||||
+5
-67
@@ -12,18 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// linker/linker.h — ELF Bionic-compatible shared library loader.
|
||||
//
|
||||
// Provides a minimal interface for loading Android ELF shared libraries
|
||||
// (.so files compiled for Bionic libc / Android ABI) into a Linux process
|
||||
// address space, resolving dependencies and exporting a symbol table.
|
||||
//
|
||||
// Design notes:
|
||||
// - Uses dlopen(3) under the hood for system libraries.
|
||||
// - For Bionic-ABI libraries (libroblox.so), manual ELF parsing and
|
||||
// mmap(2)-based loading is required; this header exposes that interface.
|
||||
// - All paths follow POSIX conventions.
|
||||
|
||||
#ifndef MOCKTAIL_LINKER_LINKER_H_
|
||||
#define MOCKTAIL_LINKER_LINKER_H_
|
||||
|
||||
@@ -34,9 +22,7 @@
|
||||
|
||||
namespace linker {
|
||||
|
||||
// Opaque handle returned by LoadLibrary. Callers must not interpret the
|
||||
// pointer value; it is only valid as an argument to ResolveSymbol or
|
||||
// UnloadLibrary.
|
||||
// Opaque; valid only with this linker's resolve/unload functions.
|
||||
using LibraryHandle = void*;
|
||||
using SymbolMap = std::unordered_map<std::string, void*>;
|
||||
using AndroidLibraryLoadObserver = bool (*)(void* context,
|
||||
@@ -50,59 +36,27 @@ struct ProgramHeaderValidation {
|
||||
explicit operator bool() const noexcept { return valid; }
|
||||
};
|
||||
|
||||
// AddSyntheticLibrary — registers one Android SONAME and only the exports
|
||||
// owned by that library. Dependencies of subsequently loaded Android ELF
|
||||
// files resolve these symbols through mcpelauncher-linker.
|
||||
LibraryHandle AddSyntheticLibrary(const std::string& soname,
|
||||
const SymbolMap& exports);
|
||||
|
||||
// RegisterSyntheticSymbol — stages or adds one export to a specific Android
|
||||
// SONAME. If the synthetic library is already loaded, its symbol table is
|
||||
// updated in place.
|
||||
// Updates an already loaded library in place.
|
||||
void RegisterSyntheticSymbol(const std::string& soname,
|
||||
const std::string& name, void* address);
|
||||
|
||||
// Registers Android Bionic thread-local and synchronization ABIs only
|
||||
// for synthetic libc.so. Host code keeps its glibc implementations.
|
||||
// These adapters belong to their synthetic SONAME, never the host namespace.
|
||||
void RegisterBionicPthreadKeyRuntimeForLibc();
|
||||
|
||||
// Registers Android's DSO-aware __register_atfork entrypoint only for
|
||||
// synthetic libc.so. The host pthread bridge preserves callback order but
|
||||
// cannot unregister callbacks when a guest DSO unloads.
|
||||
void RegisterBionicAtForkRuntimeForLibc();
|
||||
|
||||
// Registers Android LP64 positioned I/O and seek entrypoints only for
|
||||
// synthetic libc.so. The bridge relies on the x86-64 host's 64-bit
|
||||
// off_t contract and never exposes host large-file feature-macro aliases.
|
||||
void RegisterBionicLargeFileRuntimeForLibc();
|
||||
|
||||
// Registers host-independent implementations of Android libc exports that
|
||||
// are not uniformly supplied by glibc and musl. Every symbol remains owned by
|
||||
// synthetic libc.so and preserves the Android x86-64 ABI.
|
||||
void RegisterBionicHostLibcRuntimeForLibc();
|
||||
|
||||
// Registers Bionic sysconf name translation only for synthetic libc.so.
|
||||
// Bionic and glibc use different integer values for sysconf names.
|
||||
void RegisterBionicSysconfRuntimeForLibc();
|
||||
|
||||
// Registers the Android LP64 sigaction layout adapter only for libc.so.
|
||||
void RegisterBionicSignalRuntimeForLibc();
|
||||
|
||||
// Registers the LP64 Bionic FILE/__sF ABI and host stdio translation only for
|
||||
// synthetic libc.so.
|
||||
void RegisterBionicStdioRuntimeForLibc();
|
||||
|
||||
// Restores mcpelauncher-linker's own libdl exports after legacy host-symbol
|
||||
// discovery. Android dlopen/dlsym must never escape into glibc.
|
||||
// Android dlopen/dlsym must not escape into glibc.
|
||||
void RegisterBionicDynamicLoaderForLibdl();
|
||||
|
||||
// GetSyntheticLibrarySymbols — returns a snapshot of one SONAME's exports.
|
||||
// The copy is safe to inspect while other libraries are being registered.
|
||||
SymbolMap GetSyntheticLibrarySymbols(const std::string& soname);
|
||||
|
||||
// UpdateAndroidLibraryPath — configures the Bionic linker's dependency search
|
||||
// path. Multiple directories use the same colon-separated syntax as
|
||||
// LD_LIBRARY_PATH.
|
||||
void UpdateAndroidLibraryPath(const std::string& search_path);
|
||||
|
||||
// Installs the single process owner that validates or adapts a real Android
|
||||
@@ -112,18 +66,12 @@ bool SetAndroidLibraryLoadObserver(AndroidLibraryLoadObserver observer,
|
||||
void* context);
|
||||
void ClearAndroidLibraryLoadObserver(void* context);
|
||||
|
||||
// OpenAndroidLibrary — loads a real Android ELF and registers its handle under
|
||||
// logical_name. The containing directory is added to the Bionic dependency
|
||||
// search path before loading.
|
||||
LibraryHandle OpenAndroidLibrary(const std::string& real_path,
|
||||
const std::string& logical_name);
|
||||
|
||||
// Legacy compatibility bridge. New code should use AddSyntheticLibrary or
|
||||
// OpenAndroidLibrary so real files and synthetic SONAMEs cannot be confused.
|
||||
LibraryHandle LoadLibrary(const std::string& real_path,
|
||||
const std::string& host_alias);
|
||||
|
||||
// ResolveSymbol — looks up an exported symbol in a loaded library.
|
||||
void* ResolveSymbol(LibraryHandle handle, const std::string& symbol_name);
|
||||
|
||||
// Crashpad execution is intentionally outside Mocktail's supported JNI
|
||||
@@ -132,24 +80,15 @@ void* ResolveSymbol(LibraryHandle handle, const std::string& symbol_name);
|
||||
bool IsBlockedRobloxCrashReportNativeSymbol(
|
||||
std::string_view symbol_name) noexcept;
|
||||
|
||||
// UnloadLibrary — releases resources associated with a loaded library.
|
||||
void UnloadLibrary(LibraryHandle handle);
|
||||
|
||||
// Legacy compatibility bridge used by the current monolithic startup path.
|
||||
// New code should use RegisterSyntheticSymbol and assign every export to its
|
||||
// owning SONAME.
|
||||
void RegisterSymbol(const std::string& name, void* addr);
|
||||
|
||||
// Returns the legacy accumulated symbol table. It remains available until the
|
||||
// existing startup path is migrated to per-SONAME maps.
|
||||
const SymbolMap& GetBionicSymbols();
|
||||
|
||||
// Returns mcpelauncher-linker's Bionic dl_iterate_phdr entrypoint. Android
|
||||
// unwinders must not receive glibc's host implementation/layout.
|
||||
// Android unwinders must not receive glibc's implementation or layout.
|
||||
void* BionicDlIteratePhdrAddress();
|
||||
|
||||
// Confirms that the Bionic program-header iterator can see a manually loaded
|
||||
// Android ELF, including the unwind metadata needed for C++ exceptions.
|
||||
ProgramHeaderValidation ValidateBionicProgramHeaders(LibraryHandle handle);
|
||||
|
||||
// Returns the mapped ELF base for a loaded Android library alias. The lookup
|
||||
@@ -157,7 +96,6 @@ ProgramHeaderValidation ValidateBionicProgramHeaders(LibraryHandle handle);
|
||||
// alias or a host-dlopen library without Bionic image metadata.
|
||||
std::uintptr_t FindLoadedAndroidLibraryBase(const std::string& logical_name);
|
||||
|
||||
// SymbolTable — global registry of alias → handle mappings.
|
||||
const std::unordered_map<std::string, LibraryHandle>& GetSymbolTable();
|
||||
|
||||
} // namespace linker
|
||||
|
||||
@@ -22,9 +22,7 @@
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
// Interleaved PCM formats accepted at the host audio boundary. Endianness is
|
||||
// explicit because Android x86-64 producers use little-endian PCM regardless
|
||||
// of the host implementation chosen by SDL.
|
||||
// Android producers supply little-endian interleaved PCM.
|
||||
enum class PcmSampleFormat {
|
||||
kUnsigned8,
|
||||
kSigned16LittleEndian,
|
||||
@@ -41,11 +39,9 @@ struct PcmSpec {
|
||||
using AudioBufferReleaseCallback = void (*)(void* context, const void* data,
|
||||
std::size_t size_bytes);
|
||||
|
||||
// A PCM submission. With no release callback, Enqueue() copies the bytes
|
||||
// before returning. With a release callback, the sink may borrow the buffer;
|
||||
// the caller must keep it valid until the callback runs. A successful
|
||||
// submission invokes the callback exactly once, including when Clear() or
|
||||
// Shutdown() discards queued audio. A failed submission never invokes it.
|
||||
// Without a callback Enqueue copies the bytes. With one, a successful Enqueue
|
||||
// borrows them until exactly one release callback, including on Clear or
|
||||
// Shutdown. On failure the callback is not invoked.
|
||||
struct PcmBuffer {
|
||||
const void* data = nullptr;
|
||||
std::size_t size_bytes = 0;
|
||||
@@ -57,9 +53,7 @@ Status ValidatePcmSpec(const PcmSpec& spec);
|
||||
std::size_t BytesPerSample(PcmSampleFormat format);
|
||||
std::size_t BytesPerFrame(const PcmSpec& spec);
|
||||
|
||||
// Thread-safe PCM playback contract. Implementations perform device format
|
||||
// conversion and resampling through their host audio library; callers always
|
||||
// submit data in source_spec().
|
||||
// Thread-safe; callers submit source_spec(), and the backend converts it.
|
||||
class AudioSink {
|
||||
public:
|
||||
virtual ~AudioSink() = default;
|
||||
@@ -68,15 +62,14 @@ class AudioSink {
|
||||
virtual Status Enqueue(const PcmBuffer& buffer) = 0;
|
||||
virtual Status Pause() = 0;
|
||||
virtual Status Resume() = 0;
|
||||
// Linear gain is delegated to the host audio library. Zero mutes, one is
|
||||
// unity gain, and negative or non-finite values are rejected.
|
||||
// Zero mutes; one is unity; negative and non-finite values are invalid.
|
||||
virtual Status SetGain(float linear_gain) = 0;
|
||||
virtual Status Flush() = 0;
|
||||
virtual Status Clear() = 0;
|
||||
virtual Status GetQueuedBytes(std::size_t* size_bytes) const = 0;
|
||||
|
||||
// Idempotent. It waits for in-flight API calls and releases all borrowed
|
||||
// buffers before returning. Calls started after shutdown fail closed.
|
||||
// Idempotent. Waits for in-flight calls and borrowed buffers; later calls
|
||||
// fail closed.
|
||||
virtual void Shutdown() = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -23,14 +23,10 @@
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
// org.fmod.AudioDevice is an ordinary Java object. The JNI boundary passes
|
||||
// its jobject value through this opaque key without making the audio layer
|
||||
// depend on JNI headers or retain a JNI reference.
|
||||
// Opaque jobject identity; the audio layer never retains a JNI reference.
|
||||
using FmodJavaAudioDeviceIdentity = const void*;
|
||||
|
||||
// A factory returns a paused sink with exactly source_spec. Production uses
|
||||
// SDL3; tests can inject a deterministic sink without opening a host device.
|
||||
// factory_context must remain valid until the runtime is shut down.
|
||||
// Returns a paused sink. factory_context must outlive the runtime.
|
||||
using FmodJavaAudioSinkFactory = Status (*)(
|
||||
void* factory_context, const PcmSpec& source_spec,
|
||||
std::unique_ptr<AudioSink>* sink);
|
||||
@@ -44,8 +40,7 @@ struct FmodJavaAudioRuntimeOptions {
|
||||
std::size_t max_buffer_bytes_per_device = 64U * 1024U * 1024U;
|
||||
};
|
||||
|
||||
// Content-free counters suitable for readiness logs. They never expose PCM,
|
||||
// JNI identities, or guest pointers.
|
||||
// Contains no PCM, JNI identities, or guest pointers.
|
||||
struct FmodJavaAudioRuntimeStats {
|
||||
std::uint64_t init_attempts = 0;
|
||||
std::uint64_t initialized_devices = 0;
|
||||
@@ -65,18 +60,9 @@ struct FmodJavaAudioRuntimeStats {
|
||||
std::size_t pending_bytes = 0;
|
||||
};
|
||||
|
||||
// Thread-safe implementation of the APK's exact Java playback contract:
|
||||
//
|
||||
// AudioDevice.init(channels, sampleRate, blockSize, blockCount)Z
|
||||
// AudioDevice.write(byte[], length)V
|
||||
// AudioDevice.close()V
|
||||
//
|
||||
// Init() allocates blockCount fixed slots of blockSize frames. Every successful
|
||||
// Write() copies guest bytes synchronously into one slot, then lends that stable
|
||||
// storage to AudioSink without a second PCM copy. FmodJavaAudioRuntime performs
|
||||
// no per-write allocation for buffer storage or release ownership. A producer
|
||||
// waits with AudioTrack-style blocking backpressure when all slots are borrowed
|
||||
// instead of growing memory or dropping a block.
|
||||
// Fixed slots avoid per-write allocation. Write copies into a slot, then lends
|
||||
// it to AudioSink; a full pool applies blocking backpressure instead of
|
||||
// growing memory or dropping audio.
|
||||
class FmodJavaAudioRuntime final {
|
||||
public:
|
||||
explicit FmodJavaAudioRuntime(
|
||||
@@ -94,8 +80,7 @@ class FmodJavaAudioRuntime final {
|
||||
|
||||
FmodJavaAudioRuntimeStats GetStats() const;
|
||||
|
||||
// Idempotent. Prevents new devices/writes, shuts down every sink, and waits
|
||||
// for every borrowed owned buffer to be released before returning.
|
||||
// Waits for every borrowed buffer to be released.
|
||||
void Shutdown();
|
||||
|
||||
private:
|
||||
|
||||
@@ -25,9 +25,7 @@
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
// SDL documents subsystem initialization and shutdown as main-thread-only.
|
||||
// The process composition root owns this lifetime; audio workers only create
|
||||
// streams while the subsystem is active.
|
||||
// SDL requires subsystem startup and shutdown on the main thread.
|
||||
Status InitializeSdlAudioSubsystem();
|
||||
Status ShutdownSdlAudioSubsystem();
|
||||
|
||||
@@ -36,50 +34,37 @@ struct SdlPlaybackDevice {
|
||||
std::string name;
|
||||
};
|
||||
|
||||
// Lists physical SDL playback devices. The abstract `default` target is not a
|
||||
// physical entry and is therefore intentionally absent from this vector.
|
||||
// The virtual `default` target is not returned.
|
||||
Status ListSdlPlaybackDevices(std::vector<SdlPlaybackDevice>* devices);
|
||||
|
||||
// Resolves `default` or one exact physical device name. This pure selection
|
||||
// step is separate from SDL enumeration so configuration behavior is directly
|
||||
// testable.
|
||||
Status ResolveSdlPlaybackDevice(
|
||||
std::string_view requested,
|
||||
const std::vector<SdlPlaybackDevice>& available_devices,
|
||||
std::uint32_t* playback_device_id, std::string* resolved_name);
|
||||
|
||||
// Sets the process playback target used by every subsequently created FMOD
|
||||
// Java and OpenSL sink. It must run after SDL audio initialization and before
|
||||
// the first sink is opened.
|
||||
// Must run after SDL audio initialization and before the first sink opens.
|
||||
Status ConfigureSdlPlaybackDevice(
|
||||
std::string_view requested,
|
||||
std::vector<SdlPlaybackDevice>* available_devices,
|
||||
std::string* resolved_name);
|
||||
|
||||
// Returns the process playback target currently used by new sinks. The
|
||||
// physical ID is zero for SDL's system-default route.
|
||||
// Device ID zero means the system default.
|
||||
Status GetConfiguredSdlPlaybackDevice(std::uint32_t* playback_device_id,
|
||||
std::string* resolved_name);
|
||||
|
||||
// Atomically migrates every live FMOD Java and OpenSL stream to one physical
|
||||
// SDL device, or to the system default when playback_device_id is zero. New
|
||||
// sinks inherit the same target. Existing queued PCM stays in its stream.
|
||||
// Migrates live streams without dropping queued PCM. New sinks inherit it.
|
||||
Status SwitchSdlPlaybackDevice(std::uint32_t playback_device_id,
|
||||
std::string* resolved_name);
|
||||
|
||||
struct SdlAudioSinkOptions {
|
||||
PcmSpec source_spec;
|
||||
|
||||
// Zero selects the process playback target configured above (the SDL
|
||||
// default until explicitly configured). A nonzero ID bypasses that target.
|
||||
// Zero selects the configured process target.
|
||||
std::uint32_t playback_device_id = 0;
|
||||
bool start_paused = true;
|
||||
};
|
||||
|
||||
// Opens a real SDL 3.4+ playback device and its bound SDL_AudioStream. The SDL
|
||||
// audio subsystem must already be owned by InitializeSdlAudioSubsystem(). SDL
|
||||
// owns all format conversion, resampling, and borrowed-buffer consumption.
|
||||
// On failure, *sink remains null.
|
||||
// SDL owns conversion, resampling, and borrowed-buffer consumption.
|
||||
Status CreateSdlAudioSink(const SdlAudioSinkOptions& options,
|
||||
std::unique_ptr<AudioSink>* sink);
|
||||
|
||||
|
||||
@@ -26,9 +26,7 @@
|
||||
namespace mocktail {
|
||||
namespace graphics {
|
||||
|
||||
// Vulkan VkResult values are a stable signed 32-bit ABI. Keeping the Android
|
||||
// boundary independent of the optional Vulkan SDK lets capability-only builds
|
||||
// continue to compile with SDL's opaque Vulkan handles.
|
||||
// Stable signed 32-bit ABI, independent of the optional Vulkan SDK.
|
||||
using AndroidVulkanResult = VkResult;
|
||||
|
||||
inline constexpr AndroidVulkanResult kAndroidVulkanSuccess =
|
||||
@@ -43,43 +41,29 @@ inline constexpr std::uint64_t kHostImageAcquireWatchdogTimeoutNs =
|
||||
inline constexpr char kAndroidSurfaceExtension[] =
|
||||
"VK_KHR_android_surface";
|
||||
|
||||
// Desktop compositors can report a valid acquired/presented image as
|
||||
// suboptimal while a fullscreen configure settles. The Android client path
|
||||
// used by Roblox treats that positive success code as a hard error, so the
|
||||
// compatibility boundary exposes it as the equivalent VK_SUCCESS result.
|
||||
// Roblox treats VK_SUBOPTIMAL_KHR as a hard error, so expose it as success.
|
||||
VkResult NormalizeAndroidSwapchainResult(VkResult result);
|
||||
|
||||
// Android requests an infinite image-acquire wait, but a lost Wayland/X11
|
||||
// compositor wakeup would then block Roblox's RenderJob forever. Only the
|
||||
// infinite guest request is bounded; explicit finite Vulkan timeouts retain
|
||||
// their original contract.
|
||||
// Bounds only infinite acquire waits so a lost compositor wakeup cannot hang.
|
||||
std::uint64_t BoundHostImageAcquireTimeout(std::uint64_t requested_timeout);
|
||||
bool IsHostImageAcquireWatchdogTimeout(std::uint64_t requested_timeout,
|
||||
VkResult host_result);
|
||||
VkResult NormalizeHostImageAcquireResult(std::uint64_t requested_timeout,
|
||||
VkResult host_result);
|
||||
|
||||
// Infinite fence/timeline waits retain their guest-visible semantics, but are
|
||||
// issued to the host in finite slices so a stalled renderer produces evidence
|
||||
// while it remains blocked. VK_TIMEOUT from a slice is never returned to the
|
||||
// Android client as a completed wait.
|
||||
// Infinite guest waits use finite host slices without exposing slice timeouts.
|
||||
std::uint64_t BoundHostSynchronizationWaitTimeout(
|
||||
std::uint64_t requested_timeout);
|
||||
bool ShouldContinueHostSynchronizationWait(std::uint64_t requested_timeout,
|
||||
VkResult host_result);
|
||||
|
||||
// Rewrites an Android VkInstance extension request for the host WSI. The
|
||||
// returned storage owns its strings and can be used to build the
|
||||
// ppEnabledExtensionNames array passed to the host vkCreateInstance call.
|
||||
// Returned storage owns the translated extension names.
|
||||
Status TranslateAndroidVulkanInstanceExtensions(
|
||||
const std::vector<std::string>& android_extensions,
|
||||
const std::vector<std::string>& host_wsi_extensions,
|
||||
std::vector<std::string>* host_extensions);
|
||||
|
||||
// Android WSI boundary. vkCreateAndroidSurfaceKHR-compatible exports can
|
||||
// delegate here after vkCreateInstance has used the translated extension list.
|
||||
// The Android native-window member is intentionally ignored: SDL owns the real
|
||||
// host window and is the only component allowed to create its VkSurfaceKHR.
|
||||
// Ignores the guest native-window field; SDL alone creates the host surface.
|
||||
class AndroidVulkanWsiAdapter final {
|
||||
public:
|
||||
using PresentCallback = void (*)(void* user_data);
|
||||
|
||||
@@ -12,14 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Exact-path loader for Mocktail's Bionic libEGL adapter.
|
||||
//
|
||||
// A host EGL implementation may have the Android SONAME "libEGL.so" too
|
||||
// (Chromium ANGLE does). Loading by SONAME after SDL/ANGLE starts can then
|
||||
// return the host implementation and make Android code create a second EGL
|
||||
// surface for one native window. This adapter opens the Mocktail
|
||||
// bridge beside the executable and exposes only its EGL exports for the
|
||||
// synthetic Bionic libEGL.so dependency.
|
||||
// Loading by SONAME can select host ANGLE instead of the Bionic adapter. This
|
||||
// bridge therefore opens the library beside the executable by exact path.
|
||||
|
||||
#ifndef MOCKTAIL_GRAPHICS_BIONIC_EGL_BRIDGE_H_
|
||||
#define MOCKTAIL_GRAPHICS_BIONIC_EGL_BRIDGE_H_
|
||||
@@ -37,9 +31,7 @@ public:
|
||||
BionicEglBridge(const BionicEglBridge &) = delete;
|
||||
BionicEglBridge &operator=(const BionicEglBridge &) = delete;
|
||||
|
||||
// Loads $ORIGIN/libEGL.so by its exact absolute path. On success, the
|
||||
// handle intentionally remains live for the process lifetime because the
|
||||
// Bionic linker retains the exported function addresses.
|
||||
// The handle stays live because the Bionic linker retains its exports.
|
||||
bool Load();
|
||||
|
||||
bool IsLoaded() const { return handle_ != nullptr; }
|
||||
|
||||
@@ -30,9 +30,8 @@
|
||||
namespace mocktail {
|
||||
namespace graphics {
|
||||
|
||||
// Thin host WSI scaffold. It does not create a VkInstance, device, swapchain,
|
||||
// renderer, or allocator. A future Android Vulkan adapter may translate
|
||||
// vkCreateAndroidSurfaceKHR into CreateSurface after rewriting extensions.
|
||||
// Provides SDL's host WSI entry points. Instance, device, swapchain, renderer,
|
||||
// and allocator ownership stays with the caller.
|
||||
class SdlVulkanWsi final {
|
||||
public:
|
||||
SdlVulkanWsi() = default;
|
||||
|
||||
@@ -18,19 +18,15 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
// Stable process-local ABI between the host input runtime in the executable
|
||||
// and libvulkan.so, which is loaded as a separate Android compatibility DSO.
|
||||
// Pixels are tightly packed RGBA8. The producer owns and securely clears its
|
||||
// storage; consumers must copy a matching revision before returning.
|
||||
// Process-local ABI with libvulkan.so. Pixels are RGBA8; consumers must copy a
|
||||
// matching revision before the producer clears its storage.
|
||||
struct MocktailTextOverlayFrameInfo {
|
||||
static constexpr std::uint32_t kAbiVersion = 2;
|
||||
|
||||
std::uint32_t abi_version = kAbiVersion;
|
||||
std::uint32_t visible = 0;
|
||||
std::uint64_t revision = 0;
|
||||
// x/y/width/height use the host's logical input coordinate space. These
|
||||
// dimensions let a Vulkan consumer map that space to the swapchain's pixel
|
||||
// extent without assuming a 1:1 high-DPI scale.
|
||||
// Coordinates are logical, not swapchain pixels.
|
||||
std::uint32_t coordinate_width = 0;
|
||||
std::uint32_t coordinate_height = 0;
|
||||
std::int32_t x = 0;
|
||||
@@ -43,9 +39,7 @@ struct MocktailTextOverlayFrameInfo {
|
||||
|
||||
extern "C" {
|
||||
|
||||
// Lock-free conservative hint for the Vulkan present fast path. A false
|
||||
// result guarantees that no drawable overlay is currently published. A true
|
||||
// result only means that the synchronized query/copy path must decide.
|
||||
// false guarantees no overlay; true still requires a synchronized query.
|
||||
bool mocktail_text_overlay_may_present();
|
||||
bool mocktail_text_overlay_query(MocktailTextOverlayFrameInfo* frame);
|
||||
bool mocktail_text_overlay_copy(std::uint64_t revision, void* rgba,
|
||||
|
||||
@@ -22,13 +22,8 @@
|
||||
namespace mocktail {
|
||||
namespace graphics {
|
||||
|
||||
// Composites the process-local RGBA text surface into application-owned
|
||||
// swapchain images. The adapter retains ownership of every Vulkan object it
|
||||
// registers. DestroySwapchain and DestroyDevice must be called before the
|
||||
// corresponding host Vulkan destroy function.
|
||||
//
|
||||
// An unsupported device or swapchain remains a valid registration failure:
|
||||
// QueuePresent then forwards the unmodified VkPresentInfoKHR to fallback.
|
||||
// Registered Vulkan objects remain caller-owned and must be unregistered
|
||||
// before host destruction. Unsupported registrations fall back cleanly.
|
||||
class VulkanTextOverlayCompositor final {
|
||||
public:
|
||||
VulkanTextOverlayCompositor();
|
||||
@@ -38,10 +33,8 @@ class VulkanTextOverlayCompositor final {
|
||||
VulkanTextOverlayCompositor& operator=(const VulkanTextOverlayCompositor&) =
|
||||
delete;
|
||||
|
||||
// enabled_features describes the features actually enabled by
|
||||
// vkCreateDevice, not merely the features supported by the physical device.
|
||||
// The supplied proc-address callbacks must bypass the Android-facing Vulkan
|
||||
// adapter and resolve host functions directly.
|
||||
// enabled_features must match vkCreateDevice. Proc callbacks resolve host
|
||||
// functions directly, bypassing the Android adapter.
|
||||
bool RegisterDevice(VkInstance instance, VkPhysicalDevice physical_device,
|
||||
VkDevice device, std::uint32_t api_version,
|
||||
const char* const* enabled_extensions,
|
||||
@@ -61,10 +54,7 @@ class VulkanTextOverlayCompositor final {
|
||||
const VkImage* images, std::uint32_t image_count);
|
||||
void DestroySwapchain(VkDevice device, VkSwapchainKHR swapchain);
|
||||
|
||||
// Serializes adapter-mediated submit, sparse-bind, present, and idle
|
||||
// operations on the exact queue range imported by libplacebo. Other
|
||||
// VkQueues remain independent, including the inactive-overlay fast path on
|
||||
// a non-imported queue.
|
||||
// Adapter operations serialize only queues imported by libplacebo.
|
||||
VkResult QueueSubmit(VkQueue queue, std::uint32_t submit_count,
|
||||
const VkSubmitInfo* submits, VkFence fence,
|
||||
PFN_vkQueueSubmit fallback);
|
||||
@@ -77,10 +67,7 @@ class VulkanTextOverlayCompositor final {
|
||||
VkResult QueueWaitIdle(VkQueue queue, PFN_vkQueueWaitIdle fallback);
|
||||
VkResult DeviceWaitIdle(VkDevice device, PFN_vkDeviceWaitIdle fallback);
|
||||
|
||||
// Always invokes fallback when it is non-null. The original wait semaphores
|
||||
// are forwarded unchanged until the compositor successfully submits the
|
||||
// wait-collapse bridge; after that point only compositor-owned semaphores
|
||||
// are passed to fallback.
|
||||
// After wait collapse, fallback receives only compositor-owned semaphores.
|
||||
VkResult QueuePresent(VkQueue queue, const VkPresentInfoKHR* present_info,
|
||||
PFN_vkQueuePresentKHR fallback);
|
||||
|
||||
|
||||
@@ -40,9 +40,7 @@ enum class AuthRuntimeStatus {
|
||||
kUnavailable,
|
||||
};
|
||||
|
||||
// Process-local ownership for the canonical .ROBLOSECURITY header. The class
|
||||
// is move-only and clears its backing allocation before release.
|
||||
// Callers must never log the value returned by view() or c_str().
|
||||
// Move-only credential storage that clears its allocation. Never log its view.
|
||||
class SecureRobloxCredential final {
|
||||
public:
|
||||
SecureRobloxCredential() = default;
|
||||
@@ -64,14 +62,10 @@ class SecureRobloxCredential final {
|
||||
std::vector<char> bytes_;
|
||||
};
|
||||
|
||||
// Clears a transient string that contained derived credential material.
|
||||
void SecurelyClearString(std::string* value);
|
||||
|
||||
// Binds the production pseudo-VM to the exact credential resolved by the
|
||||
// authentication preflight. The binding is intentionally non-movable:
|
||||
// it must be created only after its credential reaches its final address and
|
||||
// destroyed before that credential or the VM. Guest credentials block legacy
|
||||
// env/disk fallback until the sink durably accepts a native sign-in value.
|
||||
// Must be created after the credential reaches its final address and destroyed
|
||||
// before the credential or VM. Guest bindings block legacy env/disk fallback.
|
||||
class ScopedRobloxCredentialBinding final {
|
||||
public:
|
||||
ScopedRobloxCredentialBinding(jnivm::VM* jni_vm,
|
||||
@@ -93,9 +87,7 @@ class ScopedRobloxCredentialBinding final {
|
||||
jnivm::VM* jni_vm_ = nullptr;
|
||||
};
|
||||
|
||||
// Result of resolving host credentials before the Android runtime starts.
|
||||
// Credential ownership is retained only so the exact credential validated by
|
||||
// AuthService can be moved into native startup without reopening its source.
|
||||
// Retains the validated credential without reopening its source.
|
||||
struct AuthRuntimeComposition {
|
||||
AuthRuntimeStatus status = AuthRuntimeStatus::kUnavailable;
|
||||
std::shared_ptr<jnivm::VM> jni_vm;
|
||||
@@ -107,21 +99,14 @@ struct AuthRuntimeComposition {
|
||||
explicit operator bool() const { return jni_vm != nullptr; }
|
||||
};
|
||||
|
||||
// Resolves the saved Roblox session and constructs the production pseudo-VM.
|
||||
// A VM is returned only for an authenticated session or when guest startup is
|
||||
// explicitly enabled with MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP. An exact HTTP 401
|
||||
// from a managed Mocktail file clears only the rejected .ROBLOSECURITY. A
|
||||
// rejected default Sober credential is suppressed by a private fingerprint
|
||||
// without modifying Sober's file. The guest policy then allows native sign-in
|
||||
// to continue. Explicit overrides, unsafe recovery, other invalid credentials,
|
||||
// and unavailable authentication still fail closed before native startup.
|
||||
// A VM requires authentication or explicit guest mode. HTTP 401 clears only a
|
||||
// managed credential; rejected Sober credentials are suppressed without
|
||||
// changing Sober's file.
|
||||
AuthRuntimeComposition ComposeAuthRuntime(const Environment& environment,
|
||||
const RuntimePaths& paths,
|
||||
services::AuthService& auth_service);
|
||||
|
||||
// Production overload retaining the HTTP transport for credentials delivered
|
||||
// by native sign-in after startup. A persisted credential is validated before
|
||||
// its account identity is promoted into the running pseudo-VM.
|
||||
// Retains HTTP transport for native sign-in and validates persisted credentials.
|
||||
AuthRuntimeComposition ComposeAuthRuntime(
|
||||
const Environment& environment, const RuntimePaths& paths,
|
||||
services::AuthService& auth_service,
|
||||
|
||||
@@ -21,19 +21,12 @@
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Applies Mocktail's mandatory privacy boundary to the flat Roblox
|
||||
// client-settings override object. The policy disables native Crashpad, Java
|
||||
// crash/ANR uploads, Lua Backtrace errors, and inferred-crash submission while
|
||||
// preserving unrelated caller overrides. Unsafe values are replaced rather
|
||||
// than treated as conflicts because crash upload is not a user-configurable
|
||||
// runtime capability.
|
||||
// Disables Roblox crash uploads while preserving unrelated overrides.
|
||||
bool MergeCrashReportClientSettingsOverrides(std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
|
||||
// Applies the same mandatory values to Roblox's separate AppBridge
|
||||
// fastFlagsJson channel. Only FastVariable names are inserted here;
|
||||
// Java-only client settings remain on the client-settings boundary above.
|
||||
// AppBridge accepts only the FastVariable subset of this policy.
|
||||
bool MergeCrashReportFastFlagsOverrides(std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
|
||||
@@ -28,9 +28,7 @@ enum class DeviceClass {
|
||||
kConsole,
|
||||
};
|
||||
|
||||
// One immutable profile keeps Roblox admission, pseudo-Android hardware
|
||||
// identity, and built-in input capabilities consistent. The guest ABI and
|
||||
// client-settings group remain Android/GoogleAndroidApp.
|
||||
// Admission changes do not change the Android guest ABI.
|
||||
struct DeviceProfile {
|
||||
std::string name;
|
||||
std::string cache_key;
|
||||
@@ -51,18 +49,14 @@ struct DeviceProfile {
|
||||
};
|
||||
|
||||
inline constexpr std::string_view kDefaultDeviceProfileName = "pc-windows-11";
|
||||
// These identifiers select Roblox server-side admission only. They never
|
||||
// replace the Android APK ABI or its GoogleAndroidApp client-settings group.
|
||||
// Transport classifier only; the guest remains GoogleAndroidApp.
|
||||
inline constexpr std::string_view kRobloxDesktopHttpUserAgent =
|
||||
"Roblox/WinInet";
|
||||
// The current Android-host playability endpoint recognizes this as console.
|
||||
// PlayStation-labelled user agents currently fall back to desktop admission,
|
||||
// so the PS5 marketing profile keeps this transport-only classifier.
|
||||
// The playability endpoint recognizes this classifier as console.
|
||||
inline constexpr std::string_view kRobloxConsoleAdmissionUserAgent =
|
||||
"Roblox/XboxOne";
|
||||
|
||||
// Accepts canonical names and the convenient pc/mobile/console aliases.
|
||||
// Returned profiles have static lifetime.
|
||||
// Accepts canonical names and pc/mobile/console aliases; result is static.
|
||||
const DeviceProfile* FindDeviceProfile(std::string_view name);
|
||||
bool IsValidDeviceProfileValue(std::string_view value, std::size_t maximum);
|
||||
std::string BuildCustomDeviceProfileCacheKey(const DeviceProfile& profile);
|
||||
|
||||
@@ -28,14 +28,10 @@ namespace runtime {
|
||||
inline constexpr std::size_t kMaximumPendingExternalLaunches = 8;
|
||||
|
||||
struct ExternalLaunchBrokerOptions {
|
||||
// Empty selects the current user's protected runtime endpoint. Tests and
|
||||
// isolated launchers may provide an explicit socket under a private
|
||||
// directory owned by the current user.
|
||||
// Empty selects the current user's protected endpoint.
|
||||
std::filesystem::path socket_path;
|
||||
std::size_t maximum_pending_launches = kMaximumPendingExternalLaunches;
|
||||
// The owner crosses configuration/bootstrap and a possible cgroup
|
||||
// re-exec before it can safely ACK. A concurrent browser click waits through
|
||||
// that bounded startup window instead of racing a not-yet-created socket.
|
||||
// Covers bootstrap and a possible cgroup re-exec.
|
||||
int forwarding_timeout_ms = 30000;
|
||||
};
|
||||
|
||||
@@ -49,16 +45,11 @@ struct ExternalLaunchSink {
|
||||
bool valid() const { return dispatch != nullptr; }
|
||||
};
|
||||
|
||||
// Resolves the per-user, per-installation endpoint. Owner resolution may
|
||||
// create only the private fallback directory; it never removes a stale
|
||||
// socket. Socket cleanup belongs exclusively to StartOwnerAfterLockAcquired
|
||||
// after the caller owns the matching Mocktail single-instance lock.
|
||||
// Resolution never removes stale sockets; only a lock-owning broker may do so.
|
||||
Status ResolveExternalLaunchSocketPath(bool owner,
|
||||
std::filesystem::path* socket_path);
|
||||
|
||||
// Owns a bounded process-local launch queue and its same-user AF_UNIX endpoint.
|
||||
// Wire requests contain only already-normalized ExperienceProtocol JSON; the
|
||||
// raw browser URI and its gameinfo ticket never cross this boundary.
|
||||
// The wire format contains normalized requests, never raw browser tickets.
|
||||
class ExternalLaunchBroker final {
|
||||
public:
|
||||
~ExternalLaunchBroker();
|
||||
@@ -68,25 +59,20 @@ class ExternalLaunchBroker final {
|
||||
ExternalLaunchBroker(ExternalLaunchBroker&&) = delete;
|
||||
ExternalLaunchBroker& operator=(ExternalLaunchBroker&&) = delete;
|
||||
|
||||
// The caller must hold the single-instance lock. Only this operation may
|
||||
// remove a stale socket left by a crashed owner. An optional initial request
|
||||
// is normalized and queued before the listener worker can accept later
|
||||
// browser clicks, preserving click order without a startup race.
|
||||
// Requires the single-instance lock. The initial request is queued before
|
||||
// the listener starts, preserving click order.
|
||||
static Status StartOwnerAfterLockAcquired(
|
||||
ExternalLaunchBrokerOptions options,
|
||||
std::shared_ptr<ExternalLaunchBroker>* broker,
|
||||
const RobloxExperienceLaunchRequest* initial_request = nullptr);
|
||||
|
||||
// Forwards one normalized request and waits for an explicit owner ACK. The
|
||||
// operation never starts a second runtime and never logs request contents.
|
||||
// Waits for an owner ACK and never logs the request.
|
||||
static Status ForwardToOwner(const ExternalLaunchBrokerOptions& options,
|
||||
const RobloxExperienceLaunchRequest& request);
|
||||
|
||||
Status QueueInitialRequest(RobloxExperienceLaunchRequest request);
|
||||
|
||||
// Dispatches at most maximum_requests in FIFO order. A failed dispatch is
|
||||
// returned to the front of the queue, so temporary downstream backpressure
|
||||
// never drops a browser launch.
|
||||
// A failed dispatch returns to the front of the FIFO queue.
|
||||
Status Drain(const ExternalLaunchSink& sink, std::size_t maximum_requests);
|
||||
|
||||
Status Shutdown();
|
||||
@@ -100,9 +86,7 @@ class ExternalLaunchBroker final {
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
// Process-global attachment used by the composition root and the dynamic
|
||||
// ExperienceProtocol runtime. shared_ptr acquisition keeps the broker alive
|
||||
// while an external request is being drained.
|
||||
// shared_ptr acquisition keeps the active broker alive during a drain.
|
||||
Status InstallActiveExternalLaunchBroker(
|
||||
const std::shared_ptr<ExternalLaunchBroker>& broker);
|
||||
void ClearActiveExternalLaunchBroker(const ExternalLaunchBroker* broker);
|
||||
|
||||
@@ -22,19 +22,14 @@
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Returns whether an interactive launch may present a desktop failure dialog.
|
||||
// Readiness canaries and headless sessions remain strictly non-interactive.
|
||||
// Canaries and headless sessions remain non-interactive.
|
||||
bool FailureDialogsEnabled(const Environment& environment);
|
||||
|
||||
// Presents one synchronous GTK failure dialog. This is intended for startup
|
||||
// failures that happen before the crash monitor can be armed.
|
||||
bool ShowFailureDialog(const Environment& environment,
|
||||
std::string_view message);
|
||||
|
||||
// Owns a small out-of-process GTK monitor. The helper is started before guest
|
||||
// threads exist and receives an explicit success disposition. If Mocktail
|
||||
// returns an error, calls _Exit(), or dies from a fatal signal, the helper
|
||||
// remains able to present the last safe user-facing message.
|
||||
// The helper starts before guest threads, so it can report errors and fatal
|
||||
// exits even when the main process can no longer show a dialog.
|
||||
class FailureDialogMonitor final {
|
||||
public:
|
||||
FailureDialogMonitor() = default;
|
||||
|
||||
@@ -87,9 +87,9 @@ using ResumeGameFn = Status (*)(void* context, const GameSurface& surface);
|
||||
using LeaveGameFn = Status (*)(void* context);
|
||||
using ShutdownGameRuntimeFn = Status (*)(void* context);
|
||||
|
||||
// native boundary. All callbacks are synchronous and non-throwing. The
|
||||
// coordinator serializes lifecycle invocation, matching MainGameActivity's
|
||||
// single-thread executor without embedding JNI details in the policy layer.
|
||||
// These callbacks form the native boundary. They are synchronous and
|
||||
// non-throwing; the coordinator serializes lifecycle calls to match
|
||||
// MainGameActivity's single-thread executor.
|
||||
struct GameSessionCapabilities {
|
||||
void* context = nullptr;
|
||||
SetGameForegroundFn set_game_foreground = nullptr;
|
||||
@@ -171,16 +171,11 @@ struct GameSessionSnapshot {
|
||||
std::string failure_reason;
|
||||
};
|
||||
|
||||
// Fail-closed policy for the MainGameActivity game-session branch.
|
||||
//
|
||||
// SurfaceCreated must prepare a generation before SurfaceChanged can supply its
|
||||
// native window and dimensions. QueueJoinRequest and the prepared surface are
|
||||
// independent initial prerequisites; whichever completes last starts the game
|
||||
// with the current candidate surface. Initial start never calls ResumeGame.
|
||||
// Once running, SurfaceChanged updates the active surface, SurfaceDestroyed
|
||||
// pauses, and SurfaceCreated(new generation) plus SurfaceChanged resumes.
|
||||
// Candidate surfaces become active only after the corresponding native call
|
||||
// succeeds.
|
||||
// SurfaceCreated must open a generation before its matching SurfaceChanged can
|
||||
// supply a window and size. A join request and the first surface may arrive in
|
||||
// either order; initial startup calls StartGame, never ResumeGame. Once running,
|
||||
// new generations pause and resume the session. A candidate surface becomes
|
||||
// active only after its native callback succeeds.
|
||||
class GameSessionCoordinator final {
|
||||
public:
|
||||
explicit GameSessionCoordinator(GameSessionCapabilities capabilities,
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// adapter kept outside the research runtime. The opaque context must be
|
||||
// the production jnivm::VM owned by the startup composition.
|
||||
// The opaque context must be the jnivm::VM owned by startup composition.
|
||||
bool SetJnivmPlatformWebCallbacks(
|
||||
void* context, std::shared_ptr<void> callback_context,
|
||||
void (*on_data_model_notification)(void*, JNIEnv*, jstring, jstring),
|
||||
|
||||
@@ -34,8 +34,7 @@ enum class PhysicsWorkerMode {
|
||||
bool ParsePhysicsWorkerMode(std::string_view value, PhysicsWorkerMode* mode);
|
||||
std::string_view PhysicsWorkerModeName(PhysicsWorkerMode mode);
|
||||
|
||||
// Makes every available physical core eligible for Roblox scheduler work.
|
||||
// At least one scheduler worker remains on malformed topology input.
|
||||
// Malformed topology still yields one worker.
|
||||
int CalculateThroughputWorkerCount(int available_physical_cores);
|
||||
|
||||
struct PerformancePolicy {
|
||||
@@ -61,25 +60,16 @@ PerformancePolicy ParsePerformancePolicy(
|
||||
std::string_view memory_limit_mb = "0", std::string_view game_mode = "auto",
|
||||
std::string_view physics_worker_mode = "auto");
|
||||
|
||||
// Counts unique physical cores available to the current process. Linux CPU
|
||||
// affinity is respected; systems without readable topology fall back to the
|
||||
// available logical CPU count.
|
||||
// Respects CPU affinity and falls back to the available logical count.
|
||||
int DetectAvailablePhysicalCoreCount();
|
||||
|
||||
// Merges the supported Roblox scheduler/rendering mode into the flat
|
||||
// client-settings override object. Disabled policy leaves existing overrides
|
||||
// intact. Conflicting explicit values fail closed instead of silently
|
||||
// replacing the caller's settings.
|
||||
// Disabled policy preserves overrides; conflicting values fail.
|
||||
bool MergePerformanceClientSettingsOverrides(const PerformancePolicy& policy,
|
||||
std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
|
||||
// Applies every runtime policy in a stable order before the resulting
|
||||
// override object crosses the legacy startup boundary. The mandatory
|
||||
// HttpClient compatibility policy is applied before the mandatory crash-report
|
||||
// upload block. Neither policy can be overridden by a caller-provided client
|
||||
// setting.
|
||||
// Applies mandatory HTTP and crash policies after caller settings.
|
||||
bool MergeRuntimeClientSettingsOverrides(const FrameRatePolicy& frame_rate,
|
||||
const PerformancePolicy& performance,
|
||||
std::string_view base_json,
|
||||
|
||||
@@ -143,11 +143,10 @@ struct RobloxBrowserServiceCallbackFactory {
|
||||
}
|
||||
};
|
||||
|
||||
// equivalent of the current APK's yg.c and gi.a BrowserService bridges.
|
||||
// It binds the four exact MemStorage keys and owns every Callback and
|
||||
// Connection until a disconnect-and-release shutdown has completed. Outgoing
|
||||
// WebView events are queued without JNI and must be drained by the platform
|
||||
// main thread.
|
||||
// Host counterpart to the APK's yg.c and gi.a BrowserService bridges. It binds
|
||||
// the four expected MemStorage keys. Shutdown disconnects before releasing its
|
||||
// callbacks and connections. The platform main thread drains outgoing WebView
|
||||
// events without holding JNI references.
|
||||
class RobloxBrowserServiceBridge final {
|
||||
public:
|
||||
RobloxBrowserServiceBridge(JniEnvironmentProvider environment,
|
||||
|
||||
@@ -28,7 +28,8 @@ inline constexpr std::size_t kMaximumRobloxLaunchUriBytes = 64 * 1024;
|
||||
|
||||
// Converts the two Roblox website launch protocols into the owned
|
||||
// ExperienceProtocol contract used by the supported game-session pipeline.
|
||||
// Browser authentication tickets and tracker identifiers are // discarded: the runtime uses its already validated account identity.
|
||||
// Authentication tickets and tracker IDs are discarded; the runtime already
|
||||
// has a validated account identity.
|
||||
Status ParseRobloxLaunchUri(std::string_view uri,
|
||||
RobloxExperienceLaunchRequest* request);
|
||||
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// host copy of the current APK's NativeTextBoxInfo Java value. The copy
|
||||
// contains no text and remains valid after the JNI local reference is released.
|
||||
// Host-owned NativeTextBoxInfo copy that outlives the JNI local reference.
|
||||
struct RobloxNativeTextBoxInfoSnapshot {
|
||||
float x = 0.0F;
|
||||
float y = 0.0F;
|
||||
|
||||
@@ -78,7 +78,7 @@ std::optional<NetworkProxyConfig> ParseNetworkProxyConfig(
|
||||
std::string_view host, std::string_view port);
|
||||
std::string BuildNetworkProxyUrl(const NetworkProxyConfig& proxy);
|
||||
|
||||
// Immutable, supported runtime options. Experimental MOCKTAIL_PATCH_* knobs
|
||||
// Immutable, supported runtime options. Advanced MOCKTAIL_PATCH_* controls
|
||||
// intentionally do not belong here; they remain isolated in the legacy path.
|
||||
class RuntimeConfig {
|
||||
public:
|
||||
|
||||
@@ -37,7 +37,7 @@ struct HostAbiDerivationResult {
|
||||
}
|
||||
};
|
||||
|
||||
// Derives an exact, experimental HostAbi profile by matching normalized x86-64
|
||||
// Derives an exact candidate HostAbi profile by matching normalized x86-64
|
||||
// instruction signatures against one already approved reference payload. This
|
||||
// is static analysis only; the result still requires two real Tier C canaries
|
||||
// before normal runtime activation.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<component type="desktop-application">
|
||||
<id>space.bigrat.mocktail</id>
|
||||
<name>Mocktail</name>
|
||||
<summary>Experimental Roblox Android compatibility runtime for Linux</summary>
|
||||
<summary>Play Roblox on Linux</summary>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>Apache-2.0</project_license>
|
||||
<developer id="space.bigrat">
|
||||
@@ -13,29 +13,30 @@
|
||||
<url type="homepage">https://github.com/komaruworld/mocktail</url>
|
||||
<description>
|
||||
<p>
|
||||
Mocktail is an experimental compatibility runtime for running the
|
||||
Android x86-64 Roblox client on a native Linux desktop. It provides the
|
||||
pseudo-JVM, Android ABI adapters, SDL window/input/audio integration and
|
||||
Vulkan presentation needed by the client.
|
||||
Sign in to Roblox, browse experiences, and play them in a desktop window.
|
||||
Mocktail handles graphics, audio, mouse, and keyboard input on Linux.
|
||||
</p>
|
||||
<p>
|
||||
The project is a prototype and is not affiliated with or endorsed by
|
||||
Roblox Corporation. The verified Roblox payload is downloaded on first
|
||||
launch and is not distributed with Mocktail.
|
||||
On first launch, Mocktail downloads the Roblox client and checks its
|
||||
signature. It keeps the last working version in case an update fails.
|
||||
</p>
|
||||
<p>
|
||||
Mocktail is an independent community project. It is not affiliated with
|
||||
or endorsed by Roblox Corporation, and the Roblox client is not included.
|
||||
</p>
|
||||
</description>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/main/assets/screenshots/flatpak-home.png</image>
|
||||
<caption>Browse Roblox experiences in the desktop client</caption>
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/88b7382577aa138a2d16619b091f69abcdf7da15/assets/screenshots/flatpak-home.png</image>
|
||||
<caption>Browse Roblox experiences</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/main/assets/screenshots/flatpak-gameplay-tower.png</image>
|
||||
<caption>Roblox gameplay running through Mocktail</caption>
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/88b7382577aa138a2d16619b091f69abcdf7da15/assets/screenshots/flatpak-gameplay-tower.png</image>
|
||||
<caption>Play Tower of Hell on Linux</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/main/assets/screenshots/flatpak-gameplay-lobby.png</image>
|
||||
<caption>A Roblox experience running through Mocktail</caption>
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/88b7382577aa138a2d16619b091f69abcdf7da15/assets/screenshots/flatpak-gameplay-lobby.png</image>
|
||||
<caption>Play DOORS on Linux</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<launchable type="desktop-id">space.bigrat.mocktail.desktop</launchable>
|
||||
|
||||
@@ -59,7 +59,7 @@ VERSION_MARKER_PATTERN = re.compile(
|
||||
|
||||
|
||||
class ProviderError(RuntimeError):
|
||||
"""A safe, user-facing provider failure."""
|
||||
"""Raised when a provider response cannot be downloaded or validated."""
|
||||
|
||||
|
||||
def _has_version_record(metadata: bytes, version: str) -> bool:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
"""Download a provenance-pinned Uptodown XAPK bootstrap archive."""
|
||||
"""Download a pinned Uptodown XAPK for Mocktail's first-run setup."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -47,7 +47,7 @@ DOWNLOAD_ORIGIN = "https://dw.uptodown.com/dwn/"
|
||||
|
||||
|
||||
class ProviderError(RuntimeError):
|
||||
"""A safe, user-facing provider failure."""
|
||||
"""Raised when a provider response cannot be downloaded or validated."""
|
||||
|
||||
|
||||
class DownloadPageParser(HTMLParser):
|
||||
|
||||
@@ -60,7 +60,7 @@ Branch=stable
|
||||
Url=${BASE_URL}/repo/
|
||||
RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
Homepage=https://github.com/komaruworld/mocktail
|
||||
Comment=Experimental Linux compatibility runtime
|
||||
Comment=Play Roblox on Linux
|
||||
Icon=${BASE_URL}/mocktail.svg
|
||||
GPGKey=${GPG_KEY}
|
||||
IsRuntime=false
|
||||
@@ -96,7 +96,7 @@ cat >"${OUTPUT}/index.html" <<'EOF'
|
||||
<a class="button" href="mocktail.flatpakref">Install with Flatpak</a>
|
||||
<a class="button secondary" href="https://github.com/komaruworld/mocktail">Source</a>
|
||||
<code>flatpak install --user https://mocktail.bigrat.space/mocktail.flatpakref</code>
|
||||
<p>Mocktail is experimental and is not affiliated with Roblox Corporation.</p>
|
||||
<p>Mocktail is an independent community project and is not affiliated with Roblox Corporation.</p>
|
||||
</main></body>
|
||||
</html>
|
||||
EOF
|
||||
@@ -6,47 +6,9 @@
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# scripts/build.sh — Mocktail one-shot native build script.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/build.sh [OPTIONS]
|
||||
#
|
||||
# Options:
|
||||
# --apk <path> Explicitly extract a local Roblox x86_64 APK.
|
||||
# --apk-url <url> Explicitly download and extract an APK URL.
|
||||
# --skip-apk Compatibility alias for the default managed-payload mode.
|
||||
# --build-dir <path> CMake build directory. Relative paths use project root.
|
||||
# Default: build.
|
||||
# --cmake-toolchain <file>
|
||||
# Explicit CMake toolchain file.
|
||||
# --cmake-sysroot <dir>
|
||||
# Explicit compiler/CMake sysroot.
|
||||
# --build-type <type> CMake build type: Debug | Release | RelWithDebInfo.
|
||||
# Default: Release.
|
||||
# --jobs <n> Parallel build jobs. Default: nproc.
|
||||
# --run-tests Run CTest after build.
|
||||
# --clean Remove the build/ directory before configuring.
|
||||
# --help Show this help and exit.
|
||||
#
|
||||
# Examples:
|
||||
# # Build with a local APK file:
|
||||
# ./scripts/build.sh --apk ~/Downloads/roblox-x86_64.apk
|
||||
#
|
||||
# # Build with a direct download URL:
|
||||
# ./scripts/build.sh --apk-url "https://example.com/roblox-x86_64.apk"
|
||||
#
|
||||
# # Rebuild the C++ project; the first launch obtains the managed payload:
|
||||
# ./scripts/build.sh --skip-apk --build-type Debug --run-tests
|
||||
#
|
||||
# With no APK option, Mocktail builds only the native runtime. Its first normal
|
||||
# launch downloads, validates, canaries, and activates a supported x86_64
|
||||
# payload in the user's XDG data directory. ROBLOX_LIB_PATH remains an explicit
|
||||
# research override and disables APK extraction when already set.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colour helpers
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
@@ -91,7 +53,6 @@ Examples:
|
||||
EOF
|
||||
}
|
||||
|
||||
# Default configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
BUILD_DIR="${PROJECT_ROOT}/build"
|
||||
@@ -107,7 +68,6 @@ CLEAN=false
|
||||
CMAKE_TOOLCHAIN_FILE=""
|
||||
CMAKE_SYSROOT=""
|
||||
|
||||
# Argument parsing
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--apk)
|
||||
@@ -180,7 +140,6 @@ info "Mocktail build: ${BUILD_TYPE}, -j${JOBS}"
|
||||
info "Project root: ${PROJECT_ROOT}"
|
||||
info "Build directory: ${BUILD_DIR}"
|
||||
|
||||
# Step 1 — Check required tools
|
||||
step "1/5 checking dependencies"
|
||||
|
||||
check_tool() {
|
||||
@@ -208,7 +167,6 @@ if [[ "${EXTRACT_APK}" == true && -n "${APK_URL}" ]] &&
|
||||
check_tool wget "sudo apt install wget"
|
||||
fi
|
||||
|
||||
# Step 2 — Download APK (if URL provided)
|
||||
if [[ "${EXTRACT_APK}" == true && -n "${APK_URL}" ]]; then
|
||||
step "2/5 downloading Roblox APK"
|
||||
APK_PATH="${PROJECT_ROOT}/rbx_bin/roblox-x86_64.apk"
|
||||
@@ -223,7 +181,6 @@ if [[ "${EXTRACT_APK}" == true && -n "${APK_URL}" ]]; then
|
||||
success "APK downloaded."
|
||||
fi
|
||||
|
||||
# Step 3 — Extract libroblox.so from APK
|
||||
LIBROBLOX_DST="${RBX_BIN_DIR}/libroblox.so"
|
||||
|
||||
if [[ -n "${ROBLOX_LIB_PATH:-}" ]]; then
|
||||
@@ -251,7 +208,6 @@ else
|
||||
success "No APK or libroblox.so is required at build time."
|
||||
fi
|
||||
|
||||
# Step 4 — Init git submodules
|
||||
step "4/5 initialising git submodules"
|
||||
cd "${PROJECT_ROOT}"
|
||||
if [[ -f ".gitmodules" ]]; then
|
||||
@@ -262,7 +218,6 @@ else
|
||||
info "To add submodules run: ./scripts/add_submodules.sh"
|
||||
fi
|
||||
|
||||
# Step 5 — CMake configure
|
||||
step "5/5 configuring CMake"
|
||||
|
||||
if [[ "${CLEAN}" == true && -d "${BUILD_DIR}" ]]; then
|
||||
@@ -293,21 +248,18 @@ cmake "${cmake_arguments[@]}"
|
||||
|
||||
success "CMake configuration complete."
|
||||
|
||||
# Step 6 — Build
|
||||
step "building Mocktail"
|
||||
|
||||
cmake --build "${BUILD_DIR}" -j"${JOBS}"
|
||||
|
||||
success "Build complete → ${BUILD_DIR}/mocktail"
|
||||
|
||||
# Step 7 — Run tests (optional)
|
||||
if [[ "${RUN_TESTS}" == true ]]; then
|
||||
step "running unit tests"
|
||||
ctest --test-dir "${BUILD_DIR}" --output-on-failure -j"${JOBS}"
|
||||
success "All tests passed."
|
||||
fi
|
||||
|
||||
# Done
|
||||
echo
|
||||
success "Build successful."
|
||||
echo "Run:"
|
||||
|
||||
@@ -13,12 +13,11 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Derive a fail-closed HostAbi profile from one exact approved payload.
|
||||
"""Build a HostAbi profile for one approved payload without running it.
|
||||
|
||||
The analyzer never executes guest code and never marks a payload supported.
|
||||
It accepts only an exact reference sidecar bound to the reference ELF digest,
|
||||
matches native boundaries structurally, and emits an experimental manifest for
|
||||
an isolated updater probation run.
|
||||
The reference sidecar must match the reference ELF digest. The resulting
|
||||
manifest is meant for an updater probation run; it does not mark the payload as
|
||||
supported.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -40,7 +39,7 @@ from typing import Any, Iterator, Sequence
|
||||
try:
|
||||
import capstone
|
||||
from capstone import x86 as capstone_x86
|
||||
except ImportError: # The CLI turns this into one explicit fail-closed error.
|
||||
except ImportError:
|
||||
capstone = None
|
||||
capstone_x86 = None
|
||||
|
||||
@@ -107,7 +106,7 @@ PRIMARY_TOOLCHAIN_MARKERS = (
|
||||
|
||||
|
||||
class AnalyzerError(RuntimeError):
|
||||
"""One deterministic fail-closed analysis error."""
|
||||
"""Raised when HostAbi analysis rejects an input."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -1636,9 +1635,7 @@ def analyze(arguments: argparse.Namespace) -> tuple[dict[str, Any], dict[str, An
|
||||
|
||||
def parse_arguments(argv: Sequence[str]) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Derive an exact experimental Roblox HostAbi sidecar for isolated probation"
|
||||
)
|
||||
description="Derive a probationary Roblox HostAbi sidecar"
|
||||
)
|
||||
parser.add_argument("--reference-lib", required=True, metavar="REF")
|
||||
parser.add_argument("--reference-profile", required=True, metavar="REFJSON")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Progressive real-startup harness for Mocktail.
|
||||
# Tiers A, B, and LEGACY are research experiments. Tier C verifies the
|
||||
# Tiers A, B, and LEGACY are diagnostic modes. Tier C verifies the
|
||||
# supported LuaApp graphics path. GAME verifies the local UGCGame graphics
|
||||
# path. INPUT adds a real SDL-to-JNI click to that gate. NETWORK verifies an
|
||||
# authenticated public-place join without exposing
|
||||
@@ -360,7 +360,7 @@ SUPPORT_BUNDLE_DISABLED="${MOCKTAIL_DISABLE_SUPPORT_BUNDLE:-0}"
|
||||
export MOCKTAIL_DISABLE_SUPPORT_BUNDLE=1
|
||||
set +e
|
||||
# Roblox may ignore SIGTERM while a native engine thread is
|
||||
# active. Bound the smoke experiment even in that state so the caller gets a
|
||||
# active. Bound the smoke run even in that state so the caller gets a
|
||||
# deterministic result and no stale window/process is left behind. Tier C with
|
||||
# an explicitly disabled present timer is the supported interactive launch,
|
||||
# however, and must remain alive until the user closes it.
|
||||
|
||||
@@ -200,7 +200,7 @@ InstallHostAbiExperiment(uintptr_t library_base, const HostAbiProfile &profile,
|
||||
result.uses_native_mimalloc = !options.install_allocator_bridges;
|
||||
if (library_base == 0 || profile.bridge_entry_count == 0 ||
|
||||
profile.bridge_entry_count > profile.bridge_entries.size()) {
|
||||
result.error = "invalid host ABI experiment profile";
|
||||
result.error = "invalid host ABI profile";
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ InstallHostAbiExperiment(uintptr_t library_base, const HostAbiProfile &profile,
|
||||
std::cout << std::flush;
|
||||
result.installed = ok;
|
||||
if (!ok) {
|
||||
result.error = "one or more host ABI experiment actions failed";
|
||||
result.error = "one or more host ABI actions failed";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ PayloadCompatibilityResult CheckPayloadCompatibility(
|
||||
result.error = "This Roblox build is not enabled for normal runs: " +
|
||||
result.profile.reason +
|
||||
"\n Use --allow-unverified-build only for an explicit "
|
||||
"compatibility experiment.";
|
||||
"compatibility run.";
|
||||
result.profile = {};
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@ BackendSelection SelectGraphicsBackend(
|
||||
reason};
|
||||
}
|
||||
|
||||
// Fallback order remains Vulkan-first and deterministic.
|
||||
constexpr GraphicsBackendKind kFallbackOrder[] = {
|
||||
GraphicsBackendKind::kAngleVulkan,
|
||||
GraphicsBackendKind::kDirectVulkan,
|
||||
|
||||
+6
-24
@@ -12,8 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// src/jnivm/jnivm.cc — Implementation of the Pseudo Java Native Interface VM.
|
||||
|
||||
#include "jnivm/jnivm.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -35,8 +33,6 @@
|
||||
|
||||
namespace jnivm {
|
||||
|
||||
// Class implementation
|
||||
|
||||
void Class::RegisterMethod(const std::string& method_name,
|
||||
const std::string& signature,
|
||||
MethodCallback callback) {
|
||||
@@ -71,7 +67,6 @@ 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 pointer to the active VM instance for JNI callback resolution.
|
||||
VM* g_vm_instance = nullptr;
|
||||
std::recursive_mutex g_jni_state_mutex;
|
||||
|
||||
@@ -495,8 +490,7 @@ RobloxAuthIdentity ResearchRobloxIdentityFromEnvironment() {
|
||||
return identity;
|
||||
}
|
||||
|
||||
// Temporary research-only compatibility path. Production startup should
|
||||
// inject a resolved identity through VM::SetRobloxAuthIdentity instead.
|
||||
// Normal startup injects identity through VM::SetRobloxAuthIdentity.
|
||||
identity.user_id = static_cast<int64_t>(parsed);
|
||||
const char* username = std::getenv("MOCKTAIL_ROBLOX_USERNAME");
|
||||
if (username && username[0] != '\0') {
|
||||
@@ -557,8 +551,7 @@ std::shared_ptr<Class> ClassFromJClass(jclass clazz) {
|
||||
return FallbackClassForName(cls->GetName());
|
||||
}
|
||||
|
||||
// Stores a shared_ptr into my_segment, keeping it alive in g_segment_owners.
|
||||
// Returns the encoded segment-table handle cast to jclass.
|
||||
// g_segment_owners keeps the encoded class handle alive.
|
||||
static jclass StoreClass(std::shared_ptr<Class> cls) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
Class* raw_ptr = cls.get();
|
||||
@@ -593,9 +586,8 @@ PseudoJavaObject* PseudoObjectFromRef(jobject obj) {
|
||||
raw_ptr = reinterpret_cast<Object*>(obj);
|
||||
}
|
||||
if (!raw_ptr) return nullptr;
|
||||
// Perform a static cast instead of dynamic_cast to avoid calling the host
|
||||
// __dynamic_cast against potentially-Bionic RTTI structures. All objects
|
||||
// stored in our segment table are PseudoJavaObjects by construction.
|
||||
// Avoid host __dynamic_cast on potentially Bionic RTTI; segment entries are
|
||||
// PseudoJavaObjects by construction.
|
||||
return static_cast<PseudoJavaObject*>(raw_ptr);
|
||||
}
|
||||
|
||||
@@ -840,8 +832,7 @@ std::string JavaStringUtf8Bytes(const std::vector<jchar>& utf16) {
|
||||
code_point = 0x10000 + ((code_point - 0xd800) << 10) +
|
||||
(utf16[++index] - 0xdc00);
|
||||
} else if (code_point >= 0xd800 && code_point <= 0xdfff) {
|
||||
// java.nio.charset.CharsetEncoder replaces each malformed UTF-16 input
|
||||
// unit with its configured UTF-8 replacement, the single byte '?'.
|
||||
// CharsetEncoder replaces each malformed UTF-16 unit with '?'.
|
||||
output.push_back('?');
|
||||
continue;
|
||||
}
|
||||
@@ -5082,9 +5073,7 @@ JNIEnv* VM::GetJNIEnv() {
|
||||
}
|
||||
|
||||
void VM::RestoreFunctions() {
|
||||
// Force env->functions back to our pseudo-VM table on ALL known JNIEnv*.
|
||||
// libroblox.so replaces env->functions with a Bionic JNI table during
|
||||
// JNI_OnLoad; we need to restore it before Stage 6 calls.
|
||||
// JNI_OnLoad replaces env->functions; restore all known environments.
|
||||
java_vm_storage_.functions = &invoke_interface_;
|
||||
java_vm_ = &java_vm_storage_;
|
||||
if (jni_env_) {
|
||||
@@ -5121,10 +5110,6 @@ std::shared_ptr<Class> VM::FindClass(const std::string& class_name) const {
|
||||
}
|
||||
|
||||
void VM::InitJNIFunctionTables() {
|
||||
// JNIInvokeInterface_ — the JavaVM function table.
|
||||
// Most functions are no-ops in this pseudo-VM; only AttachCurrentThread
|
||||
// is meaningful for single-threaded prototype use.
|
||||
|
||||
invoke_interface_.AttachCurrentThread =
|
||||
[](JavaVM* vm, void** env, void* /*args*/) -> jint {
|
||||
if (JniVmTraceEnabled()) {
|
||||
@@ -5207,9 +5192,6 @@ void VM::InitJNIFunctionTables() {
|
||||
java_vm_storage_.functions = &invoke_interface_;
|
||||
java_vm_ = &java_vm_storage_;
|
||||
|
||||
// JNINativeInterface_ — the JNIEnv function table.
|
||||
// Only the minimal subset required by Roblox's JNI_OnLoad is stubbed here.
|
||||
|
||||
native_interface_.FindClass =
|
||||
[](JNIEnv* /*env*/, const char* name) -> jclass {
|
||||
if (JniVmTraceEnabled()) {
|
||||
|
||||
+45
-135
@@ -12,18 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Transitional Sober compatibility runtime. This file preserves the current
|
||||
// reverse-engineered startup path while upstream-backed modules replace
|
||||
// it one boundary at a time.
|
||||
//
|
||||
// Implements the preserved 5-stage initialisation pipeline:
|
||||
//
|
||||
// Stage 1: Initialise Pseudo-JVM (libjnivm).
|
||||
// Stage 2: Register mandatory Android SDK JNI classes.
|
||||
// Stage 3: Load host system libraries under Bionic alias names.
|
||||
// Stage 4: Map libroblox.so into process address space via the linker.
|
||||
// Stage 5: Invoke JNI_OnLoad and hand control to the game engine.
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
@@ -400,7 +388,6 @@ extern "C" int mocktail_recover_stack_chk_fail() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// JNI_OnLoad function pointer type as specified by the JNI standard.
|
||||
using JniOnLoadFn = jint (*)(JavaVM*, void*);
|
||||
using NativeGameGlobalInitFn = void (*)(JNIEnv*, jclass);
|
||||
using NativeInitClientSettingsFn = jint (*)(JNIEnv*, jclass, jstring, jstring,
|
||||
@@ -2519,10 +2506,7 @@ static void* GetThreadScratchBuffer(pid_t tid) {
|
||||
return g_thread_scratch_pool[0].buffer;
|
||||
}
|
||||
|
||||
// Fallback vtable used when a null/invalid object pointer is dereferenced
|
||||
// during a vtable dispatch in Stage 6. Each slot is a no-op stub that
|
||||
// returns 0 so that the caller can continue execution. The table covers
|
||||
// all vtable offsets seen in libroblox (currently up to +0xf8 = slot 31).
|
||||
// Covers every libroblox vtable offset observed through slot 31 (+0xf8).
|
||||
static uintptr_t NullVtableStub() { return 0; }
|
||||
static const uintptr_t kFallbackVtable[32] = {
|
||||
reinterpret_cast<uintptr_t>(&NullVtableStub),
|
||||
@@ -2558,8 +2542,7 @@ static const uintptr_t kFallbackVtable[32] = {
|
||||
reinterpret_cast<uintptr_t>(&NullVtableStub),
|
||||
reinterpret_cast<uintptr_t>(&NullVtableStub),
|
||||
};
|
||||
// Fake object whose first 8 bytes point to kFallbackVtable.
|
||||
// Used to satisfy `mov rax,[rsi]; call [rax+offset]` when RSI=null.
|
||||
// Lets a recovered `mov rax,[rsi]` continue through the fallback vtable.
|
||||
static const uintptr_t kFallbackObject[4] = {
|
||||
reinterpret_cast<uintptr_t>(kFallbackVtable), 0, 0, 0,
|
||||
};
|
||||
@@ -2810,12 +2793,10 @@ void ApplyRuntimeDefaults() {
|
||||
SetEnvDefault("MOCKTAIL_ENGINE_DETACH", "0");
|
||||
SetEnvDefault("MOCKTAIL_INIT_CLIENT_SETTINGS", "0");
|
||||
SetEnvDefault("MOCKTAIL_POST_CLIENT_SETTINGS", "0");
|
||||
// Sober-compatible fallback: allow unauthenticated startup (LuaApp path)
|
||||
// unless explicitly disabled.
|
||||
SetEnvDefault("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
||||
// Current Roblox Android builds drive app startup through NativeGLInterface's
|
||||
// V2 app bridge path. The legacy NativeAppBridgeInterface entry point stays
|
||||
// available for experiments, but running it in parallel can block V2 init.
|
||||
// available by opt-in, but running it in parallel can block V2 init.
|
||||
SetEnvDefault("MOCKTAIL_APP_BRIDGE_APP_START", "0");
|
||||
SetEnvDefault("MOCKTAIL_APP_BRIDGE_APP_START_THREAD", "0");
|
||||
SetEnvDefault("MOCKTAIL_START_LUA_APP_DM", "1");
|
||||
@@ -5824,8 +5805,8 @@ extern "C" void* mocktail_roblox_allocator_object_alloc_bridge(void*,
|
||||
return mocktail_roblox_aligned_alloc_bridge(size, align);
|
||||
}
|
||||
|
||||
// Retained only for the legacy fixed-offset allocator patch below. Current
|
||||
// Build-ID experiments own their seed storage in compat/host_abi_experiment.
|
||||
// Retained for the legacy fixed-offset allocator patch. Build-ID profiles keep
|
||||
// their seed storage in compat/host_abi_experiment.
|
||||
uintptr_t g_mocktail_roblox_allocator_object_vtable[4] = {};
|
||||
uintptr_t g_mocktail_roblox_allocator_object[1] = {
|
||||
reinterpret_cast<uintptr_t>(g_mocktail_roblox_allocator_object_vtable)};
|
||||
@@ -5902,7 +5883,7 @@ bool InstallActiveHostAbiExperiment(uintptr_t libroblox_base) {
|
||||
}
|
||||
g_host_abi_install_attempted = true;
|
||||
if (!HostAbiExperimentRequested()) {
|
||||
std::cout << " [compat] host ABI experiment disabled by policy\n"
|
||||
std::cout << " [compat] host ABI profile disabled by policy\n"
|
||||
<< std::flush;
|
||||
return false;
|
||||
}
|
||||
@@ -5910,7 +5891,7 @@ bool InstallActiveHostAbiExperiment(uintptr_t libroblox_base) {
|
||||
const mocktail::compat::HostAbiProfile* profile =
|
||||
g_active_host_abi_profile.load(std::memory_order_acquire);
|
||||
if (libroblox_base == 0 || profile == nullptr) {
|
||||
std::cerr << " [compat] host ABI experiment has no active profile/base\n"
|
||||
std::cerr << " [compat] host ABI install has no active profile/base\n"
|
||||
<< std::flush;
|
||||
return false;
|
||||
}
|
||||
@@ -6534,7 +6515,7 @@ extern "C" void mocktail_before_soinfo_constructors(const char* realpath,
|
||||
|
||||
// Host alloc bridges must be in place before any constructor executes.
|
||||
if (HostAbiExperimentRequested() && !InstallActiveHostAbiExperiment(base)) {
|
||||
std::cerr << " [compat] host ABI experiment failed before constructors\n"
|
||||
std::cerr << " [compat] host ABI install failed before constructors\n"
|
||||
<< std::flush;
|
||||
return;
|
||||
}
|
||||
@@ -6634,7 +6615,6 @@ extern "C" void mocktail_before_soinfo_constructors(const char* realpath,
|
||||
uintptr_t* init_array =
|
||||
reinterpret_cast<uintptr_t*>(base + init_array_offset);
|
||||
|
||||
// Make the page writable just in case
|
||||
long ctor_page_size = sysconf(_SC_PAGESIZE);
|
||||
if (ctor_page_size > 0) {
|
||||
uintptr_t addr = reinterpret_cast<uintptr_t>(init_array);
|
||||
@@ -6648,7 +6628,6 @@ extern "C" void mocktail_before_soinfo_constructors(const char* realpath,
|
||||
g_original_ctors.reserve(kCtorCount);
|
||||
for (size_t i = 0; i < kCtorCount; ++i) {
|
||||
g_original_ctors.push_back(init_array[i]);
|
||||
// Skip the first constructor if env flag is set, otherwise wrap it
|
||||
if (i == 0 && IsEnabled("MOCKTAIL_PATCH_FIRST_CONSTRUCTOR")) {
|
||||
static void (*noop_fn)() = []() {};
|
||||
g_original_ctors[i] = reinterpret_cast<uintptr_t>(noop_fn);
|
||||
@@ -26926,26 +26905,21 @@ void HeadlessSegvHandler(int signo, siginfo_t* info, void* context) {
|
||||
return;
|
||||
}
|
||||
|
||||
// libGLdispatch crash recovery (handles Bionic TLS incompatibility in Glibc threads).
|
||||
// Matches any SIGSEGV occurring inside libGLdispatch.so.0.
|
||||
// libGLdispatch may access Bionic TLS keys through glibc thread state.
|
||||
if (g_current_stage >= 6 && module_name && std::strstr(module_name, "libGLdispatch") != nullptr) {
|
||||
// If the instruction is a 64-bit store (mov [reg], reg) or load (mov reg, [reg]):
|
||||
if (instruction[0] == 0x48 && (instruction[1] == 0x89 || instruction[1] == 0x8b)) {
|
||||
int rm = instruction[2] & 7;
|
||||
int reg_map[] = { REG_RAX, REG_RCX, REG_RDX, REG_RBX, -1, -1, REG_RSI, REG_RDI };
|
||||
if (rm < 8 && reg_map[rm] != -1) {
|
||||
uintptr_t old_val = ucontext->uc_mcontext.gregs[reg_map[rm]];
|
||||
// Only redirect if the register is currently NULL, very low, or points to a read-only stub/vtable.
|
||||
if (old_val < kStage5LowAddressThreshold) {
|
||||
// Get or allocate a thread-specific writable buffer from our pool.
|
||||
// This avoids calling malloc (which corrupts the heap if interrupted)
|
||||
// and avoids sharing the same static buffer across multiple threads (which causes corruption).
|
||||
// Signal handlers cannot call malloc; each thread gets fixed scratch
|
||||
// storage to avoid sharing writes across recovered threads.
|
||||
pid_t tid = static_cast<pid_t>(syscall(SYS_gettid));
|
||||
void* thread_scratch = GetThreadScratchBuffer(tid);
|
||||
ucontext->uc_mcontext.gregs[reg_map[rm]] = reinterpret_cast<greg_t>(thread_scratch);
|
||||
} else if (old_val == reinterpret_cast<uintptr_t>(&NullVtableStub) ||
|
||||
old_val == reinterpret_cast<uintptr_t>(kFallbackVtable)) {
|
||||
// If it was redirected to a read-only stub/vtable, redirect it to a thread-specific writable buffer.
|
||||
pid_t tid = static_cast<pid_t>(syscall(SYS_gettid));
|
||||
void* thread_scratch = GetThreadScratchBuffer(tid);
|
||||
ucontext->uc_mcontext.gregs[reg_map[rm]] = reinterpret_cast<greg_t>(thread_scratch);
|
||||
@@ -27016,7 +26990,7 @@ void HeadlessSegvHandler(int signo, siginfo_t* info, void* context) {
|
||||
}
|
||||
skip_stage6_null_object_immediate_store:
|
||||
|
||||
// Broader: any `48 89 ??` store with low dest-addr (catches all register combos).
|
||||
// Catch the remaining `48 89 ??` stores to low addresses.
|
||||
if (g_current_stage >= 6 && info &&
|
||||
reinterpret_cast<uintptr_t>(info->si_addr) < kStage5LowAddressThreshold &&
|
||||
instruction[0] == 0x48 && instruction[1] == 0x89) {
|
||||
@@ -27026,10 +27000,9 @@ skip_stage6_null_object_immediate_store:
|
||||
write(2, msg, sizeof(msg) - 1);
|
||||
}
|
||||
++g_skipped_headless_null_writes;
|
||||
// Determine instruction length (modrm) — skip the store.
|
||||
// 48 89 /r — 3 or 4 bytes depending on displacement.
|
||||
uint8_t modrm = instruction[2];
|
||||
uint8_t mod = (modrm >> 6) & 0x3;
|
||||
// The ModR/M displacement determines how many instruction bytes to skip.
|
||||
int instr_len = (mod == 1) ? 4 : (mod == 2) ? 7 : 3;
|
||||
ucontext->uc_mcontext.gregs[REG_RIP] += instr_len;
|
||||
return;
|
||||
@@ -27054,9 +27027,7 @@ skip_stage6_null_object_immediate_store:
|
||||
reinterpret_cast<uintptr_t>(info->si_addr) == 0 &&
|
||||
instruction[0] == 0x48 && instruction[1] == 0x8b &&
|
||||
instruction[2] == 0x06) {
|
||||
// Pattern: mov rax,[rsi] with RSI=null, often followed by call [rax+N].
|
||||
// Point RSI at a fake object whose vtable is kFallbackVtable so that the
|
||||
// load succeeds and subsequent indirect calls land in NullVtableStub.
|
||||
// Redirect the null receiver so a following vtable call reaches the stub.
|
||||
if (g_skipped_headless_null_writes == 0) {
|
||||
const char msg[] =
|
||||
" [patch] redirected Stage6 mov rax,[rsi] (RSI=null) to fallback object\n";
|
||||
@@ -27065,13 +27036,11 @@ skip_stage6_null_object_immediate_store:
|
||||
++g_skipped_headless_null_writes;
|
||||
ucontext->uc_mcontext.gregs[REG_RSI] =
|
||||
reinterpret_cast<greg_t>(kFallbackObject);
|
||||
// Do NOT advance RIP — re-execute the mov against the now-valid RSI.
|
||||
// Re-run the load against the repaired receiver.
|
||||
return;
|
||||
}
|
||||
|
||||
// Broader fallback: any Stage6 null/low-address load through RSI into RAX
|
||||
// that is the exact 3-byte form `48 8b 06` — recover by pointing RSI at
|
||||
// fallback vtable object so the call chain can continue.
|
||||
// The same recovery applies to nonzero addresses below the valid threshold.
|
||||
if (g_current_stage >= 6 && info &&
|
||||
reinterpret_cast<uintptr_t>(info->si_addr) < kStage5LowAddressThreshold &&
|
||||
instruction[0] == 0x48 && instruction[1] == 0x8b &&
|
||||
@@ -27107,8 +27076,7 @@ skip_stage6_null_object_immediate_store:
|
||||
return;
|
||||
}
|
||||
|
||||
// Also handle `mov rax,[rax]` (48 8b 00) / `mov rax,[rax+N]` (48 8b 40 NN)
|
||||
// with low RAX — redirect RAX to kFallbackVtable.
|
||||
// Redirect low `mov rax,[rax+N]` bases to the fallback vtable.
|
||||
if (g_current_stage >= 6 && info &&
|
||||
reinterpret_cast<uintptr_t>(info->si_addr) < kStage5LowAddressThreshold &&
|
||||
instruction[0] == 0x48 && instruction[1] == 0x8b &&
|
||||
@@ -27280,12 +27248,8 @@ skip_stage6_null_object_immediate_store:
|
||||
return;
|
||||
}
|
||||
|
||||
// SI_KERNEL SIGTRAP (si_code=128): kernel-generated trap, commonly a
|
||||
// shadow-stack (CET/SHSTK) return-address mismatch on Roblox's internal
|
||||
// threads. The instruction at RIP is not an int3 — it's the instruction
|
||||
// that would have executed after the bad return. We cannot safely continue
|
||||
// from here; if a recovery jmp_buf is armed on this thread use it, otherwise
|
||||
// just exit the offending thread so the rest of the engine keeps running.
|
||||
// SI_KERNEL SIGTRAP commonly follows a CET shadow-stack return mismatch.
|
||||
// Use a thread-local recovery target when one is armed.
|
||||
if (signo == SIGTRAP && info && info->si_code == SI_KERNEL &&
|
||||
g_current_stage >= 6) {
|
||||
if (libroblox_base != 0 &&
|
||||
@@ -27324,17 +27288,11 @@ skip_stage6_null_object_immediate_store:
|
||||
write(2, msg, sizeof(msg) - 1);
|
||||
siglongjmp(g_start_app_with_params_jmp_buf, 1);
|
||||
}
|
||||
// No recovery target on this thread — skip the trap by advancing RIP past
|
||||
// the current instruction and returning. Killing the thread via
|
||||
// pthread_exit would hang the engine since Roblox waits on these threads.
|
||||
// SI_KERNEL SIGTRAP here is a shadow-stack (CET) mismatch on Roblox's
|
||||
// internal threads; skipping it lets execution continue from the instruction
|
||||
// after the bad return address.
|
||||
// Roblox may wait on this thread, so exiting it can hang the engine.
|
||||
const char msg[] =
|
||||
" [patch] SI_KERNEL SIGTRAP: no recovery, skipping instruction\n";
|
||||
write(2, msg, sizeof(msg) - 1);
|
||||
// Advance past the current instruction — for SI_KERNEL traps the faulting
|
||||
// instruction is still at RIP (not executed yet), so +1 moves past it.
|
||||
// SI_KERNEL leaves RIP on the instruction that has not run yet.
|
||||
ucontext->uc_mcontext.gregs[REG_RIP] += 1;
|
||||
return;
|
||||
}
|
||||
@@ -28488,9 +28446,8 @@ bool PatchStartAppDebugTrap(void* native_start_app_with_params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// NOTE: We intentionally do NOT patch the function entry point with `ret`.
|
||||
// Doing so would silently kill the real startup path. Only the known-bad
|
||||
// diagnostic tail block inside the function is bypassed here.
|
||||
// Patch only the diagnostic tail; replacing the entry point would disable
|
||||
// real startup.
|
||||
constexpr uintptr_t kNativeStartAppOffset = 0x250436d;
|
||||
constexpr uintptr_t kTailPatchOffset = 0x2504567;
|
||||
constexpr unsigned char kTailJumpPatch[] = {
|
||||
@@ -30533,9 +30490,7 @@ void* EngineStartupThread(void* arg) {
|
||||
<< std::flush;
|
||||
mocktail::window::MakeCurrentOnThread();
|
||||
}
|
||||
// Force our pseudo-VM function table on the env that EngineStartupThread will use.
|
||||
// libroblox.so replaces env->functions during JNI_OnLoad; we restore it here so
|
||||
// that our NewStringUTF / GetStringUTFChars helpers work correctly in Stage 6.
|
||||
// JNI_OnLoad replaces env->functions; Stage 6 needs the pseudo-VM table.
|
||||
if (env && context->vm) {
|
||||
context->vm->RestoreFunctions();
|
||||
env = context->vm->GetJNIEnv();
|
||||
@@ -32050,7 +32005,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
std::cerr << "[FATAL] This Roblox build is not enabled for normal runs: "
|
||||
<< build_profile.reason << '\n'
|
||||
<< " Use --allow-unverified-build only for an explicit "
|
||||
"compatibility experiment.\n";
|
||||
"compatibility check.\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
@@ -32064,10 +32019,8 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
std::cout << " [trace] full tracing enabled\n" << std::flush;
|
||||
}
|
||||
|
||||
// Disable shadow-stack (CET/SHSTK) for this process before loading
|
||||
// libroblox. Roblox's internal threads trigger SIGTRAP SI_KERNEL from
|
||||
// return-address mismatches on the shadow stack; disabling it here prevents
|
||||
// those spurious traps. This is a no-op on kernels that don't support CET.
|
||||
// Roblox internal threads trigger SI_KERNEL traps from CET shadow-stack
|
||||
// return mismatches. Unsupported kernels ignore this request.
|
||||
{
|
||||
long r = syscall(SYS_arch_prctl, ARCH_SHSTK_DISABLE, ARCH_SHSTK_SHSTK);
|
||||
if (r == 0) {
|
||||
@@ -32086,7 +32039,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
setenv("MOCKTAIL_APP_BRIDGE_HEADLESS_INIT_PARAMS", is_headless ? "1" : "0",
|
||||
1);
|
||||
}
|
||||
// Stage 1: Initialise the Bionic-compatible ELF linker + Pseudo-JVM.
|
||||
PrintStage(1, "Initialising Bionic linker + Pseudo-JVM");
|
||||
|
||||
#ifdef MOCKTAIL_USE_BIONIC_LINKER
|
||||
@@ -32101,8 +32053,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
}
|
||||
std::cout << " Pseudo-JVM instance created successfully.\n";
|
||||
|
||||
// Stage 2: Register mandatory Android SDK JNI classes.
|
||||
// Roblox's JNI_OnLoad will call FindClass for these at startup.
|
||||
// FindClass needs these descriptors before JNI_OnLoad.
|
||||
PrintStage(2, "Registering Android SDK JNI classes");
|
||||
|
||||
auto context_class = jni_vm->RegisterClass("android/content/Context");
|
||||
@@ -32116,7 +32067,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
std::cout << " [JNI callback] nativeInitClientSettings invoked\n";
|
||||
});
|
||||
|
||||
// Classes required by Stage 6 startup entry points.
|
||||
auto native_gl_interface_class =
|
||||
jni_vm->RegisterClass("com/roblox/engine/jni/NativeGLInterface");
|
||||
g_native_gl_class_for_main_thread =
|
||||
@@ -32190,10 +32140,8 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
std::cout << " Registered " << jni_vm->GetClassCount()
|
||||
<< " JNI class(es)\n";
|
||||
|
||||
// Window init (non-headless mode): create the SDL3 + EGL window before
|
||||
// Android/EGL stubs are preloaded, so SDL resolves the real host graphics
|
||||
// backend. By default the window stays hidden until Roblox presents a real
|
||||
// frame.
|
||||
// Create SDL/EGL before loading Android stubs so SDL binds the host graphics
|
||||
// backend. The window stays hidden until the first real frame.
|
||||
bool window_initialised = false;
|
||||
if (!is_headless) {
|
||||
const int win_w = runtime_config.window().width;
|
||||
@@ -32250,15 +32198,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 3: Populate the Bionic symbol table with all stub + system
|
||||
// symbols so that mcpelauncher-linker can resolve libroblox.so's
|
||||
// DT_NEEDED entries without calling the host Glibc dynamic linker.
|
||||
//
|
||||
// Strategy:
|
||||
// 1. dlopen each stub .so with RTLD_GLOBAL (host linker finds them
|
||||
// via RPATH=$ORIGIN).
|
||||
// 2. Iterate over known stub symbols and register them into the
|
||||
// Bionic symbol table via linker::RegisterSymbol().
|
||||
PrintStage(3, "Building Bionic symbol table from stubs");
|
||||
|
||||
libc_shim::Install();
|
||||
@@ -32299,7 +32238,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
}
|
||||
}
|
||||
|
||||
// Stub libraries to preload. All located via RPATH=$ORIGIN.
|
||||
std::vector<const char*> stub_names = {
|
||||
"libc.so", "libdl.so", "libm.so", "libz.so",
|
||||
"libandroid.so", "liblog.so", "libmediandk.so",
|
||||
@@ -32343,15 +32281,9 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
real_gles_handle = OpenRealGlesLibrary();
|
||||
}
|
||||
|
||||
// Register every known Android NDK / system symbol into the Bionic
|
||||
// symbol table so mcpelauncher-linker resolves them correctly.
|
||||
//
|
||||
// We prioritize resolving from our loaded stub libraries first.
|
||||
// This ensures Bionic shims (like custom pthread structure handlers
|
||||
// or __sF stdio compatibility) are preferred over host glibc ones,
|
||||
// preventing layout incompatibility and crashes.
|
||||
// Prefer Bionic shims over glibc for guest layouts such as pthread and FILE.
|
||||
static const char* kSymbolsToRegister[] = {
|
||||
// Exported stub functions and structures
|
||||
// Symbols exported by the Android compatibility stubs.
|
||||
"AAsset_close",
|
||||
"AAsset_getBuffer",
|
||||
"AAsset_getLength",
|
||||
@@ -32609,7 +32541,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
"__system_property_get",
|
||||
"__write_chk",
|
||||
|
||||
// Binary-compatible standard pthread symbols not explicitly shimmed
|
||||
// pthread functions whose host ABI is already compatible.
|
||||
"pthread_once",
|
||||
"pthread_self",
|
||||
"pthread_equal",
|
||||
@@ -32626,7 +32558,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
"pthread_sigmask",
|
||||
"pthread_setname_np",
|
||||
|
||||
// Standard C library functions needed by libroblox.so
|
||||
// Standard C library functions resolved from host libc.
|
||||
"strcmp",
|
||||
"strncmp",
|
||||
"strcpy",
|
||||
@@ -32938,13 +32870,8 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 4: Load libroblox.so via the Bionic-compatible linker.
|
||||
|
||||
// Auto-register ALL undefined symbols from the target library BEFORE
|
||||
// loading stub libraries into the Bionic linker. This ensures the stubs'
|
||||
// injected symbol maps include every symbol libroblox.so will need at
|
||||
// relocation time. Auto-registration must run first because load_library
|
||||
// takes a snapshot of g_bionic_symbols at call time.
|
||||
// Register imports before loading synthetic SONAMEs; load_library snapshots
|
||||
// g_bionic_symbols.
|
||||
{
|
||||
int fd = ::open(library_path.c_str(), O_RDONLY);
|
||||
if (fd >= 0) {
|
||||
@@ -32954,7 +32881,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
PROT_READ, MAP_PRIVATE, fd, 0);
|
||||
if (map != MAP_FAILED) {
|
||||
auto* ehdr = reinterpret_cast<Elf64_Ehdr*>(map);
|
||||
// Walk section headers to find .dynsym and .dynstr
|
||||
auto* shdr = reinterpret_cast<Elf64_Shdr*>(
|
||||
reinterpret_cast<char*>(map) + ehdr->e_shoff);
|
||||
Elf64_Shdr* dynsym_hdr = nullptr;
|
||||
@@ -32975,7 +32901,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
if (syms[si].st_shndx != SHN_UNDEF) continue;
|
||||
const char* name = strtab + syms[si].st_name;
|
||||
if (!name || name[0] == '\0') continue;
|
||||
// Skip if already registered
|
||||
if (linker::GetBionicSymbols().count(name)) continue;
|
||||
auto result = ResolveSymbolForBionic(name, has_window,
|
||||
real_gles_handle,
|
||||
@@ -32996,9 +32921,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
}
|
||||
}
|
||||
|
||||
// Register stub libraries inside Bionic linker. This must happen AFTER
|
||||
// auto-registration so that every symbol libroblox needs is already in
|
||||
// g_bionic_symbols when load_library snapshots the map.
|
||||
// Load synthetic libraries only after the import map is complete.
|
||||
linker::RegisterBionicPthreadKeyRuntimeForLibc();
|
||||
linker::RegisterBionicSysconfRuntimeForLibc();
|
||||
linker::RegisterBionicSignalRuntimeForLibc();
|
||||
@@ -33038,12 +32961,11 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
std::cout << " [linker] Bionic unwind metadata validated for libroblox\n"
|
||||
<< std::flush;
|
||||
|
||||
// The pre-constructor linker hook is the only installation phase. A second
|
||||
// arena initialization here would not be idempotent; post-load only verifies
|
||||
// that the experiment completed.
|
||||
// The pre-constructor hook owns installation; repeating arena setup here is
|
||||
// not idempotent.
|
||||
if (HostAbiExperimentRequested() &&
|
||||
(!g_host_abi_install_attempted || !g_host_abi_install_result)) {
|
||||
std::cerr << "\n[FATAL] Host ABI experiment did not install before "
|
||||
std::cerr << "\n[FATAL] Host ABI profile did not install before "
|
||||
"libroblox constructors: "
|
||||
<< (g_host_abi_install_result.error != nullptr
|
||||
? g_host_abi_install_result.error
|
||||
@@ -33052,7 +32974,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Stage 5: Invoke JNI_OnLoad — hand control to the game engine.
|
||||
PrintStage(5, "Invoking JNI_OnLoad in libroblox.so");
|
||||
|
||||
auto* jni_onload =
|
||||
@@ -33230,9 +33151,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
}
|
||||
}
|
||||
|
||||
// libroblox.so may have replaced env->functions during JNI_OnLoad with a
|
||||
// Bionic JNI table. Restore our pseudo-VM function table so that our Stage 6
|
||||
// helpers (NewStringUTF, GetStringUTFChars, etc.) work correctly.
|
||||
// Restore the pseudo-JNI table if JNI_OnLoad replaced it.
|
||||
{
|
||||
JNIEnv* main_env = jni_vm->GetJNIEnv();
|
||||
if (main_env) {
|
||||
@@ -33855,13 +33774,8 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
std::cout << " [compat] skipped fixed-offset Stage 6 patch set\n"
|
||||
<< std::flush;
|
||||
}
|
||||
// By default, all startup steps are enabled when MOCKTAIL_START_ENGINE=1.
|
||||
// Real (non-mock) calls to nativeSetAssetPath,
|
||||
// nativeAppBridgeV2InitWithParams and nativeAppBridgeV2StartAppWithParams
|
||||
// are now the default path — the mock/bypass path only activates when the
|
||||
// real call crashes and recovery fires via siglongjmp. The old
|
||||
// MOCKTAIL_UNSAFE_NATIVE_STARTUP guard is removed so users do not need to
|
||||
// set it explicitly.
|
||||
// Real startup calls run by default; signal recovery falls back to the
|
||||
// Mocktail implementations.
|
||||
const bool requested_set_asset_path =
|
||||
ShouldRunStartupStep("MOCKTAIL_STEP_SET_ASSET_PATH", true);
|
||||
const bool requested_init_with_params =
|
||||
@@ -33869,8 +33783,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
const bool requested_start_app_with_params =
|
||||
ShouldRunStartupStep("MOCKTAIL_STEP_START_APP_WITH_PARAMS", true);
|
||||
const bool run_set_asset_path = requested_set_asset_path;
|
||||
// call_real_set_asset_path: enabled by default unless explicitly disabled.
|
||||
// The signal handler will catch any crash and fall back to MocktailSetAssetPath.
|
||||
const bool call_real_set_asset_path =
|
||||
run_set_asset_path &&
|
||||
!IsDisabled("MOCKTAIL_CALL_REAL_NATIVE_SET_ASSET_PATH");
|
||||
@@ -33888,7 +33800,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
IsEnabled("MOCKTAIL_APP_BRIDGE_APP_START")) &&
|
||||
native_app_bridge_app_start != nullptr;
|
||||
const bool run_init_with_params = requested_init_with_params;
|
||||
// call_real_init_with_params: real call is the default; mock only on crash.
|
||||
const bool call_real_init_with_params =
|
||||
run_init_with_params &&
|
||||
!IsDisabled("MOCKTAIL_CALL_REAL_APP_BRIDGE_INIT");
|
||||
@@ -33934,8 +33845,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
(native_pass_current_display_refresh_rate != nullptr ||
|
||||
native_pass_supported_refresh_rates != nullptr);
|
||||
const bool run_start_app_with_params = requested_start_app_with_params;
|
||||
// call_real_start_app_with_params: opt-in because the real call can block
|
||||
// startup before any rendering path becomes visible.
|
||||
// Keep the real call opt-in because it can block before rendering starts.
|
||||
const bool call_real_start_app_with_params =
|
||||
run_start_app_with_params &&
|
||||
IsEnabled("MOCKTAIL_CALL_REAL_APP_BRIDGE_START");
|
||||
@@ -34547,7 +34457,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
|
||||
if (!build_profile.default_allowed) {
|
||||
std::cerr
|
||||
<< "[FATAL] Compatibility experiment returned, but Build ID "
|
||||
<< "[FATAL] Compatibility run returned, but Build ID "
|
||||
<< build_profile.elf_build_id
|
||||
<< " has not passed the readiness gates; refusing a success exit.\n";
|
||||
return EXIT_FAILURE;
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// src/libc_shim/libc_shim.cc — Bionic libc → Glibc shim implementation.
|
||||
|
||||
#include "libc_shim/libc_shim.h"
|
||||
|
||||
#include <array>
|
||||
@@ -33,7 +31,6 @@ namespace libc_shim {
|
||||
|
||||
namespace {
|
||||
|
||||
// Registered prefix mappings: android_prefix → host_prefix.
|
||||
std::unordered_map<std::string, std::string> g_path_mappings;
|
||||
|
||||
bool g_installed = false;
|
||||
@@ -504,10 +501,8 @@ extern "C" ssize_t mocktail_readlink(const char* path, char* buf,
|
||||
|
||||
extern "C" ssize_t mocktail___readlink_chk(const char* path, char* buf,
|
||||
size_t len, size_t buf_len) {
|
||||
// Match Bionic's fortify.cpp contract: both an impossible ssize_t result
|
||||
// and a compiler-proven destination overrun are fatal programming errors,
|
||||
// not recoverable readlink failures. Returning EINVAL here used to let the
|
||||
// guest continue after a detected buffer overflow.
|
||||
// Bionic treats impossible ssize_t results and proven overruns as fatal;
|
||||
// returning EINVAL would continue after a detected buffer overflow.
|
||||
if (len > static_cast<size_t>(SSIZE_MAX)) {
|
||||
std::fprintf(stderr,
|
||||
"FORTIFY: readlink: size %zu > SSIZE_MAX\n", len);
|
||||
|
||||
+9
-25
@@ -6,11 +6,7 @@
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// src/linker/linker.cc — ELF loader implementation.
|
||||
//
|
||||
// Stage A (no mcpelauncher-linker): delegates to host dlopen/dlsym.
|
||||
// Stage B (MOCKTAIL_USE_BIONIC_LINKER=1): delegates to mcpelauncher-linker
|
||||
// which loads Bionic-ABI ELF files without Glibc version conflicts.
|
||||
// Without the Bionic linker, loading falls back to host dlopen/dlsym.
|
||||
|
||||
#include "linker/linker.h"
|
||||
|
||||
@@ -50,16 +46,13 @@ namespace linker {
|
||||
|
||||
namespace {
|
||||
|
||||
// Global alias → handle map maintained across all LoadLibrary calls.
|
||||
std::unordered_map<std::string, LibraryHandle> g_symbol_table;
|
||||
|
||||
// New registrations retain symbol ownership per Android SONAME. The legacy
|
||||
// global map remains isolated below until src/main.cc is migrated.
|
||||
// Synthetic exports retain ownership per Android SONAME.
|
||||
std::unordered_map<std::string, SymbolMap> g_synthetic_symbols;
|
||||
std::unordered_map<std::string, LibraryHandle> g_synthetic_libraries;
|
||||
std::unordered_set<LibraryHandle> g_synthetic_handles;
|
||||
|
||||
// Compatibility symbol map used only by RegisterSymbol/LoadLibrary.
|
||||
SymbolMap g_legacy_bionic_symbols;
|
||||
|
||||
// Keep string memory alive for Bionic linker sonames to avoid dangling pointers.
|
||||
@@ -121,11 +114,8 @@ int BionicDlcloseBoundary(void* handle) {
|
||||
char* BionicDlerrorBoundary() { return ::linker::dlerror(); }
|
||||
#endif
|
||||
|
||||
// The legacy startup path still discovers most imports in one untyped map.
|
||||
// Bionic libc APIs with host-incompatible signatures or integer values must
|
||||
// have exactly one owner, synthetic libc.so. Re-exporting those host functions
|
||||
// from an earlier DT_NEEDED SONAME lets the guest bind to glibc before it
|
||||
// reaches libc.so.
|
||||
// Host-incompatible Bionic APIs belong only to synthetic libc.so. Exporting
|
||||
// them from an earlier dependency could bind the guest to glibc instead.
|
||||
bool IsBionicLibcOnlySymbol(std::string_view symbol) {
|
||||
static constexpr std::string_view kStdioSymbols[] = {
|
||||
"__fread_chk", "__fwrite_chk", "__sF", "clearerr",
|
||||
@@ -189,8 +179,6 @@ void RemoveHandleFromRegistry(LibraryHandle handle) {
|
||||
|
||||
} // namespace
|
||||
|
||||
// Public API
|
||||
|
||||
void RegisterSymbol(const std::string& name, void* addr) {
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
g_legacy_bionic_symbols[name] = addr;
|
||||
@@ -351,9 +339,8 @@ void RegisterBionicHostLibcRuntimeForLibc() {
|
||||
RegisterSyntheticSymbol(
|
||||
"libc.so", "gethostbyname",
|
||||
reinterpret_cast<void*>(mocktail_bionic_gethostbyname));
|
||||
// The preserved startup path publishes older DNS wrappers through its
|
||||
// legacy global map. Replace those addresses before libc.so snapshots that
|
||||
// map so every supported payload crosses the upload deny boundary.
|
||||
// Replace legacy DNS wrappers before libc.so snapshots the global map, so
|
||||
// every payload crosses the upload deny boundary.
|
||||
RegisterSymbol("getaddrinfo",
|
||||
reinterpret_cast<void*>(mocktail_bionic_getaddrinfo));
|
||||
RegisterSymbol("freeaddrinfo",
|
||||
@@ -375,9 +362,8 @@ void RegisterBionicHostLibcRuntimeForLibc() {
|
||||
reinterpret_cast<void*>(mocktail_bionic_mallinfo));
|
||||
RegisterSyntheticSymbol("libc.so", "strerror_r",
|
||||
reinterpret_cast<void*>(mocktail_bionic_strerror_r));
|
||||
// The preserved startup path scans libroblox imports through the legacy
|
||||
// global map before relocation. Publish the same address there so it
|
||||
// cannot auto-register glibc's GNU char*-returning strerror_r instead.
|
||||
// Publish this before relocation to prevent binding glibc's incompatible
|
||||
// GNU char*-returning strerror_r.
|
||||
RegisterSymbol("strerror_r",
|
||||
reinterpret_cast<void*>(mocktail_bionic_strerror_r));
|
||||
RegisterSyntheticSymbol("libc.so", "strtoll_l",
|
||||
@@ -643,9 +629,7 @@ LibraryHandle LoadLibrary(const std::string& real_path,
|
||||
return OpenAndroidLibrary(real_path, host_alias);
|
||||
}
|
||||
|
||||
// The legacy composition already reaches this per-SONAME boundary for
|
||||
// every Android dependency. Register host adapters here so libc.so owns the
|
||||
// guest ABI without adding calls to the preserved legacy startup TU.
|
||||
// Register adapters here so libc.so owns the guest ABI per SONAME.
|
||||
if (real_path == "libc.so") {
|
||||
RegisterBionicAtForkRuntimeForLibc();
|
||||
RegisterBionicHostLibcRuntimeForLibc();
|
||||
|
||||
+6
-17
@@ -6,10 +6,6 @@
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
// Mocktail's composition root stays intentionally small. During the
|
||||
// upstream-first migration it delegates to the isolated legacy runtime; new
|
||||
// platform, JNI, loader, and service modules are linked as explicit targets.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -454,10 +450,8 @@ int main(int argc, char* argv[]) {
|
||||
<< active_build_id.error << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
// An old local probation receipt can outlive the point where the
|
||||
// exact Build ID becomes shipped support. Built-in profiles are
|
||||
// immutable and must then use the repository manifest instead of
|
||||
// attempting an external override.
|
||||
// Built-in profiles supersede stale probation receipts and never
|
||||
// use external overrides.
|
||||
use_external_profile = mocktail::compat::FindHostAbiProfile(
|
||||
active_build_id.build_id) == nullptr;
|
||||
}
|
||||
@@ -759,11 +753,8 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun &&
|
||||
!isolated_canary) {
|
||||
// Start accepting browser requests only after cgroup re-exec, payload
|
||||
// preflight and host bridge setup have succeeded. An ACK therefore cannot
|
||||
// be invalidated by a known host-side
|
||||
// startup or exec boundary. Isolated canaries cannot receive browser
|
||||
// launches and own no public per-user endpoint.
|
||||
// Listen only after re-exec, preflight, and bridge setup, so an ACK cannot
|
||||
// precede a known startup failure. Isolated canaries expose no endpoint.
|
||||
mocktail::Status broker_status =
|
||||
mocktail::runtime::ExternalLaunchBroker::StartOwnerAfterLockAcquired(
|
||||
broker_options, &external_launch_broker.broker(),
|
||||
@@ -808,10 +799,8 @@ int main(int argc, char* argv[]) {
|
||||
command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
||||
failure_dialog.MarkSuccessful();
|
||||
support_bundle_guard.Disarm();
|
||||
// The embedded Android payload registers process-global atexit handlers
|
||||
// for workers that it does not expose for joining. The supported runtime
|
||||
// reaches this boundary only after explicit lifecycle, audio, input, and
|
||||
// SDL shutdown; do not re-enter guest teardown from the host C runtime.
|
||||
// Guest atexit handlers target workers that cannot be joined. Host
|
||||
// shutdown is already complete here, so do not re-enter guest teardown.
|
||||
std::cout.flush();
|
||||
std::cerr.flush();
|
||||
std::_Exit(EXIT_SUCCESS);
|
||||
|
||||
@@ -264,8 +264,8 @@ std::string CommandLineUsage(const std::string& program_name) {
|
||||
"and\n"
|
||||
<< " uses its native sign-in flow. New credentials are stored "
|
||||
"privately.\n\n"
|
||||
<< "Debug and experimental toggles remain available as MOCKTAIL_* env "
|
||||
"vars.\n";
|
||||
<< "Additional runtime options are available as MOCKTAIL_* environment "
|
||||
"variables.\n";
|
||||
return usage.str();
|
||||
}
|
||||
|
||||
|
||||
@@ -653,9 +653,7 @@ class DiscordRpcSession::Impl final {
|
||||
phase_ = phase;
|
||||
request_ = {};
|
||||
if (request != nullptr) {
|
||||
// Retain only the public join coordinates and private-field presence.
|
||||
// Access codes and canonical launch JSON never cross into the Discord
|
||||
// worker, even when public_servers_only is manually disabled.
|
||||
// Never pass access codes or canonical launch JSON to the Discord worker.
|
||||
request_.place_id = request->place_id;
|
||||
request_.game_instance_id = request->game_instance_id;
|
||||
if (!request->reserved_server_access_code.empty()) {
|
||||
@@ -743,9 +741,7 @@ class DiscordRpcSession::Impl final {
|
||||
place_icon_url = cached.icon_url;
|
||||
if ((cached.name.empty() || cached.icon_url.empty()) &&
|
||||
std::chrono::steady_clock::now() >= cached.next_attempt) {
|
||||
// Never leave Discord showing "Joining" while a Roblox metadata
|
||||
// request is in flight. Publish the available/fallback activity
|
||||
// first, then refine it when metadata becomes available.
|
||||
// Publish fallback activity while metadata is in flight.
|
||||
resolve_metadata_after_publish = true;
|
||||
}
|
||||
}
|
||||
@@ -829,8 +825,7 @@ class DiscordRpcSession::Impl final {
|
||||
continue;
|
||||
}
|
||||
if (changed) {
|
||||
// Re-enter the normal rate-limited publication path with the cached
|
||||
// metadata. The fallback activity is already visible during lookup.
|
||||
// Publish cached metadata through the normal rate limiter.
|
||||
published_revision = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -33,10 +33,7 @@ namespace runtime {
|
||||
namespace {
|
||||
|
||||
constexpr size_t kMaxPendingLaunchRequests = 8;
|
||||
// Loading libroblox contributes a large static-TLS image. A one-megabyte
|
||||
// pthread stack is accepted in unit tests before the guest is loaded, but
|
||||
// pthread_create returns EINVAL in production once that TLS image is present.
|
||||
// Keep this above the runtime's proven 16 MiB guest-thread floor.
|
||||
// libroblox's static TLS requires the proven 16 MiB guest-thread stack floor.
|
||||
constexpr size_t kLaunchWorkerStackSize = 64ULL * 1024 * 1024;
|
||||
constexpr std::chrono::milliseconds kWebSurfaceReadyTimeout{3000};
|
||||
constexpr char kRobloxBaseUrl[] = "https://www.roblox.com/";
|
||||
@@ -512,9 +509,7 @@ Status RobloxExperienceComposition::OpenWebSurface(
|
||||
}
|
||||
}
|
||||
|
||||
// Opening and configuring the singleton is one serialized transaction.
|
||||
// Optional fields are normalized before this point, so presentation state
|
||||
// from the previous route can never leak into the new page.
|
||||
// Serialize setup so presentation state cannot leak between routes.
|
||||
if (!current->SetTitle(presentation.title) ||
|
||||
!current->SetVisible(presentation.visible) ||
|
||||
!current->SetBackNavigationDisabled(
|
||||
@@ -626,9 +621,8 @@ Status RobloxExperienceComposition::DispatchWebViewOpen(
|
||||
presentation.visible = request.is_visible.value_or(true);
|
||||
presentation.show_domain_as_title =
|
||||
request.show_domain_as_title.value_or(false);
|
||||
// The host helper has no Android toolbar. Mapping a hidden Roblox back
|
||||
// button to disabled host back navigation preserves the protocol's only
|
||||
// actionable navigation policy without inventing a replacement header.
|
||||
// The host helper has no toolbar, so a hidden Roblox back button disables
|
||||
// host back navigation too.
|
||||
presentation.back_navigation_disabled =
|
||||
request.back_button_visible.has_value() && !*request.back_button_visible;
|
||||
return composition->OpenWebSurface(request.url, "webview",
|
||||
@@ -802,11 +796,8 @@ Status RobloxExperienceComposition::RouteWebSurfaceEvent(
|
||||
RobloxWebViewBridge* web_view_bridge) {
|
||||
switch (event.type) {
|
||||
case WebViewHelperEventType::kExecuteRoblox:
|
||||
// EnableAndroidWebViewService4 installs WebViewProtocol's listener on
|
||||
// the shared gi.a fragment regardless of which protocol opened it. The
|
||||
// MemStorage callback in gi.a is only the APK's disabled legacy
|
||||
// fallback. Keeping the raw command intact is required for
|
||||
// RequestGameJob fields such as instanceId and joinAttemptId.
|
||||
// WebViewProtocol listens on the shared gi.a fragment. Preserve the raw
|
||||
// command, including RequestGameJob instance and attempt IDs.
|
||||
if (route == WebSurfaceRoute::kNone) {
|
||||
return FailedPrecondition(
|
||||
"WebView helper event has no active logical route");
|
||||
@@ -842,9 +833,7 @@ Status RobloxExperienceComposition::RouteCurrentWebSurfaceEvent(
|
||||
logical_generation == 0 ||
|
||||
logical_generation != web_surface_logical_generation_ ||
|
||||
web_surface_route_ == WebSurfaceRoute::kNone) {
|
||||
// A close, helper replacement, or logical page reuse invalidates every
|
||||
// event already buffered for the old surface. This is normal teardown,
|
||||
// not a platform delivery failure.
|
||||
// Closing or replacing the helper invalidates buffered surface events.
|
||||
return Status::Ok();
|
||||
}
|
||||
route = web_surface_route_;
|
||||
@@ -954,8 +943,7 @@ Status RobloxExperienceComposition::PromoteAuthenticatedSession() {
|
||||
if (consumes_window_surface_events_) {
|
||||
window::WindowSurfaceEvent stale_event;
|
||||
while (window::PollWindowSurfaceEvent(&stale_event)) {
|
||||
// Readiness already captured the latest snapshot. Older events were
|
||||
// queued before a GAME consumer existed and must not replay stale state.
|
||||
// Readiness already captured the latest snapshot; discard older events.
|
||||
}
|
||||
}
|
||||
vm->SetRobloxExperienceLifecycleCallbacks(
|
||||
@@ -1112,9 +1100,8 @@ Status RobloxExperienceComposition::DrainExternalLaunchRequests() {
|
||||
Status status = DrainActiveExternalLaunchRequests(
|
||||
ExternalLaunchSink{this, &RobloxExperienceComposition::DispatchLaunch},
|
||||
available);
|
||||
// The MessageBus can fill the composition queue after the capacity snapshot
|
||||
// above. Keep the external request at the broker's front and retry on the
|
||||
// next drain instead of turning ordinary backpressure into runtime failure.
|
||||
// A MessageBus race can fill the queue; leave the request at the broker's
|
||||
// front and retry on the next drain.
|
||||
return status.code() == StatusCode::kUnavailable ? Status::Ok() : status;
|
||||
}
|
||||
|
||||
@@ -1161,8 +1148,7 @@ Status RobloxExperienceComposition::DrainLaunchRequests() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (lua_app_return_pending_ && !launch_in_progress_ &&
|
||||
active_launch_ == nullptr) {
|
||||
// Consume before invoking the controller so a concurrent callback sets
|
||||
// a new pending observation instead of being overwritten on return.
|
||||
// Consume first so a concurrent callback can set a new observation.
|
||||
lua_app_return_pending_ = false;
|
||||
returned_controller = controller_;
|
||||
}
|
||||
@@ -1198,9 +1184,8 @@ Status RobloxExperienceComposition::DrainLaunchRequests() {
|
||||
if (controlled_switch_waiting_for_return_) return Status::Ok();
|
||||
if (!launch_in_progress_ && active_launch_ == nullptr && game_active_ &&
|
||||
!pending_launch_requests_.empty()) {
|
||||
// Prevent a stale gameDidLeave callback from being applied to the next
|
||||
// controller. The queued launch starts only after the exact LuaApp
|
||||
// return callback for this controlled leave has been consumed.
|
||||
// Start the queued launch only after consuming this leave callback, so
|
||||
// it cannot reach the next controller.
|
||||
controlled_switch_waiting_for_return_ = true;
|
||||
switch_active_game = true;
|
||||
}
|
||||
|
||||
@@ -32,9 +32,7 @@ namespace runtime {
|
||||
|
||||
class RobloxWindowInputRuntime;
|
||||
|
||||
// Narrow seam between the cross-thread JNI queue and its main-thread owners.
|
||||
// Production uses RobloxWindowInputRuntime and the SDL window module. Tests
|
||||
// provide a deterministic implementation without creating an SDL window.
|
||||
// Testable boundary between the JNI queue and its main-thread owners.
|
||||
class RobloxTextInputJniBridgeBackend {
|
||||
public:
|
||||
virtual ~RobloxTextInputJniBridgeBackend() = default;
|
||||
@@ -59,19 +57,15 @@ class RobloxTextInputJniBridgeBackend {
|
||||
virtual bool RequestHideTextInput(uint64_t generation) = 0;
|
||||
};
|
||||
|
||||
// Connects pseudo-JVM keyboard callbacks to the input runtime without
|
||||
// invoking SDL or Roblox native functions from an engine callback thread.
|
||||
// Guest callbacks enqueue owned commands; the registered window pre-pump
|
||||
// callback drains them on SDL's main thread.
|
||||
// Guest callbacks enqueue commands that the SDL pre-pump drains on its main
|
||||
// thread.
|
||||
class RobloxTextInputJniBridge final {
|
||||
public:
|
||||
static Status Create(jnivm::VM* vm,
|
||||
std::shared_ptr<RobloxWindowInputRuntime> input_runtime,
|
||||
std::unique_ptr<RobloxTextInputJniBridge>* bridge);
|
||||
|
||||
// Deterministic construction seam. The backend is retained through every
|
||||
// in-flight VM callback and must obey the same main-thread contract as the
|
||||
// production window implementation.
|
||||
// The test backend remains alive through in-flight VM callbacks.
|
||||
static Status CreateForTesting(
|
||||
jnivm::VM* vm, std::shared_ptr<RobloxTextInputJniBridgeBackend> backend,
|
||||
std::unique_ptr<RobloxTextInputJniBridge>* bridge);
|
||||
@@ -81,8 +75,7 @@ class RobloxTextInputJniBridge final {
|
||||
RobloxTextInputJniBridge(const RobloxTextInputJniBridge&) = delete;
|
||||
RobloxTextInputJniBridge& operator=(const RobloxTextInputJniBridge&) = delete;
|
||||
|
||||
// Idempotently stops callbacks and clears pending sensitive text before the
|
||||
// input runtime and SDL window are destroyed.
|
||||
// Stops callbacks and clears pending sensitive text before window teardown.
|
||||
Status Shutdown();
|
||||
|
||||
private:
|
||||
|
||||
+14
-50
@@ -12,13 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// src/window/window.cc — SDL3 window + real EGL context.
|
||||
//
|
||||
// Creates a real OS window via SDL3 and obtains a real EGL display + context
|
||||
// from the selected host EGL/GLES driver (system GLVND or ANGLE/Vulkan).
|
||||
// libroblox.so's EGL calls are intercepted in libegl_stub and redirected to
|
||||
// these handles so the game engine renders into a real window.
|
||||
|
||||
#include "window/window.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
@@ -55,7 +48,7 @@
|
||||
namespace mocktail {
|
||||
namespace window {
|
||||
|
||||
// EGL type declarations (no system EGL headers to avoid conflicts with stubs)
|
||||
// Local EGL declarations avoid conflicts with the stub headers.
|
||||
|
||||
using EGLDisplay = void*;
|
||||
using EGLConfig = void*;
|
||||
@@ -98,8 +91,6 @@ static constexpr EGLDisplay EGL_NO_DISPLAY = nullptr;
|
||||
static constexpr EGLContext EGL_NO_CONTEXT = nullptr;
|
||||
static constexpr EGLSurface EGL_NO_SURFACE = nullptr;
|
||||
|
||||
// EGL function pointer types
|
||||
|
||||
using PFN_eglGetDisplay = EGLDisplay (*)(EGLNativeDisplayType);
|
||||
using PFN_eglInitialize = EGLBoolean (*)(EGLDisplay, EGLint*, EGLint*);
|
||||
using PFN_eglBindAPI = EGLBoolean (*)(EGLenum);
|
||||
@@ -121,8 +112,6 @@ using PFN_eglDestroySurface = EGLBoolean (*)(EGLDisplay, EGLSurface);
|
||||
using PFN_eglTerminate = EGLBoolean (*)(EGLDisplay);
|
||||
using PFN_eglGetProcAddress = __eglMustCastFP (*)(const char*);
|
||||
|
||||
// Module state
|
||||
|
||||
struct WindowState {
|
||||
SDL_Window* sdl_window = nullptr;
|
||||
EGLDisplay egl_display = nullptr;
|
||||
@@ -551,10 +540,8 @@ void ConfigureGraphicsBackendBeforeSDL() {
|
||||
SDL_SetHint(SDL_HINT_VIDEO_FORCE_EGL, "1");
|
||||
|
||||
if (ShouldUseAngleVulkanBackend() || ShouldUseNativeVulkanBackend()) {
|
||||
// Mesa's implicit device-select layer is useful on desktop, but it can
|
||||
// crash when ANGLE/Roblox initialise Vulkan through the Android compat
|
||||
// path. The layer's JSON advertises NODEVICE_SELECT=1 as the supported
|
||||
// opt-out, so use the real Vulkan loader/ICDs without that implicit layer.
|
||||
// Mesa's implicit device selector can crash in the Android Vulkan path;
|
||||
// its supported opt-out is NODEVICE_SELECT=1.
|
||||
setenv("NODEVICE_SELECT", "1", 0);
|
||||
setenv("DISABLE_LAYER_MESA_ANTI_LAG", "1", 0);
|
||||
}
|
||||
@@ -563,8 +550,7 @@ void ConfigureGraphicsBackendBeforeSDL() {
|
||||
ResolveConfiguredVideoDriverChoice();
|
||||
const char* video_driver = VideoDriverChoiceName(video_driver_choice);
|
||||
if (video_driver != nullptr) {
|
||||
// ResolveConfiguredVideoDriverChoice already preserves every non-empty
|
||||
// user override. Overwrite only an inherited empty variable here.
|
||||
// Preserve any non-empty user override.
|
||||
setenv("SDL_VIDEODRIVER", video_driver, 1);
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, video_driver);
|
||||
}
|
||||
@@ -654,9 +640,7 @@ void* QueryNativeWindowHandle() {
|
||||
return wl_egl_win;
|
||||
}
|
||||
|
||||
// Direct Vulkan creates VkSurfaceKHR from SDL_Window rather than an EGL
|
||||
// window. Its SDL object is therefore the stable host-native identity when
|
||||
// the backend exposes no EGL-specific Wayland handle.
|
||||
// Direct Vulkan uses SDL_Window as its stable native identity.
|
||||
return g_state.direct_vulkan ? g_state.sdl_window : nullptr;
|
||||
}
|
||||
|
||||
@@ -865,8 +849,6 @@ bool RetryWithAutoAngleFallback(int width, int height, const char* title,
|
||||
return Init(width, height, title);
|
||||
}
|
||||
|
||||
// Init
|
||||
|
||||
Status ConfigureWindowStatePersistence(const std::filesystem::path& path) {
|
||||
if (g_state.initialised) {
|
||||
return Status::Error(
|
||||
@@ -893,8 +875,7 @@ bool Init(int width, int height, const char* title) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The same-surface Vulkan overlay renders SDL_EVENT_TEXT_EDITING
|
||||
// composition text. Candidate lists remain owned by the platform IME.
|
||||
// The Vulkan overlay renders preedit text; the platform IME owns candidates.
|
||||
SDL_SetHint(SDL_HINT_IME_IMPLEMENTED_UI, "composition");
|
||||
|
||||
g_state.state_persistence_active =
|
||||
@@ -967,15 +948,12 @@ bool Init(int width, int height, const char* title) {
|
||||
g_state.direct_vulkan = true;
|
||||
g_state.initialised = true;
|
||||
ResolveNativeWindowHandle();
|
||||
// The Android handle is opaque to the guest-facing adapter. Keep it
|
||||
// non-null even when SDL does not expose an EGL-specific Wayland handle;
|
||||
// SDL_Vulkan_CreateSurface uses sdl_window directly.
|
||||
// Direct Vulkan needs a non-null opaque Android handle even without an
|
||||
// EGL-specific Wayland handle.
|
||||
if (g_state.native_window == nullptr) {
|
||||
g_state.native_window = g_state.sdl_window;
|
||||
}
|
||||
// Wayland does not configure a surface (and Vulkan reports undefined
|
||||
// currentExtent) until the window is mapped. Direct Vulkan therefore
|
||||
// cannot use the EGL-era "show on first swap" policy.
|
||||
// Wayland reports undefined currentExtent until the window is mapped.
|
||||
if (!SDL_ShowWindow(g_state.sdl_window)) {
|
||||
fprintf(stderr, " [window] SDL_ShowWindow failed: %s\n", SDL_GetError());
|
||||
SDL_DestroyWindow(g_state.sdl_window);
|
||||
@@ -1062,7 +1040,6 @@ bool Init(int width, int height, const char* title) {
|
||||
width, height, title ? title : "(null)");
|
||||
}
|
||||
|
||||
// ----- SDL3 init -----
|
||||
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||
fprintf(stderr, " [window] SDL_Init failed: %s\n", SDL_GetError());
|
||||
return false;
|
||||
@@ -1073,10 +1050,9 @@ bool Init(int width, int height, const char* title) {
|
||||
|
||||
ConfigureGraphicsBackendAfterSDLInit();
|
||||
|
||||
// Keep the hint set in case SDL rechecks it during context creation.
|
||||
// SDL may recheck this hint during context creation.
|
||||
SDL_SetHint(SDL_HINT_VIDEO_FORCE_EGL, "1");
|
||||
|
||||
// Configure SDL3 GL context for OpenGL ES 3.0 / EGL
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
|
||||
@@ -1088,7 +1064,6 @@ bool Init(int width, int height, const char* title) {
|
||||
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
|
||||
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
||||
|
||||
// Create the window
|
||||
SDL_WindowFlags window_flags =
|
||||
SDL_WINDOW_OPENGL | SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_RESIZABLE;
|
||||
if (g_state.state_persistence_active && g_state.persisted_window.fullscreen) {
|
||||
@@ -1119,11 +1094,9 @@ bool Init(int width, int height, const char* title) {
|
||||
fprintf(stderr, " [window] SDL3 window created (%dx%d)\n", width, height);
|
||||
ShowWindowAccordingToStartupMode();
|
||||
|
||||
// Create SDL GL/EGL context
|
||||
SDL_GLContext sdl_context = SDL_GL_CreateContext(g_state.sdl_window);
|
||||
if (!sdl_context && IsEnabledEnv("MOCKTAIL_ALLOW_GLES2_RESEARCH")) {
|
||||
// Current production Roblox client settings reject GLES2. Retain this
|
||||
// only as an explicit research probe for old payloads.
|
||||
// Current Roblox clients reject GLES2; this path supports older payloads.
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
|
||||
sdl_context = SDL_GL_CreateContext(g_state.sdl_window);
|
||||
}
|
||||
@@ -1145,7 +1118,6 @@ bool Init(int width, int height, const char* title) {
|
||||
return CreateSoftwareWaitingWindow(width, height, title);
|
||||
}
|
||||
|
||||
// Make the context current to initialize internal structures
|
||||
if (!SDL_GL_MakeCurrent(g_state.sdl_window, sdl_context)) {
|
||||
fprintf(stderr, " [window] SDL_GL_MakeCurrent failed: %s\n",
|
||||
SDL_GetError());
|
||||
@@ -1198,7 +1170,6 @@ bool Init(int width, int height, const char* title) {
|
||||
return CreateSoftwareWaitingWindow(width, height, title);
|
||||
}
|
||||
|
||||
// Query EGL handles from SDL3
|
||||
g_state.egl_display = SDL_EGL_GetCurrentDisplay();
|
||||
g_state.egl_config = SDL_EGL_GetCurrentConfig();
|
||||
g_state.egl_surface = SDL_EGL_GetWindowSurface(g_state.sdl_window);
|
||||
@@ -1237,7 +1208,7 @@ bool Init(int width, int height, const char* title) {
|
||||
|
||||
ResolveNativeWindowHandle();
|
||||
|
||||
// Unbind context on main thread so that engine thread can bind it
|
||||
// The render thread binds the context after the main thread releases it.
|
||||
SDL_GL_MakeCurrent(g_state.sdl_window, nullptr);
|
||||
if (WindowTraceEnabled()) {
|
||||
fprintf(stderr, " [window] SDL_GL_MakeCurrent(nullptr) on main thread\n");
|
||||
@@ -1259,8 +1230,6 @@ bool Init(int width, int height, const char* title) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// MakeCurrentOnThread — call from the engine rendering thread to bind the
|
||||
// EGL context on that thread.
|
||||
bool MakeCurrentOnThread() {
|
||||
if (!g_state.initialised || !g_state.egl_context) {
|
||||
if (WindowTraceEnabled()) {
|
||||
@@ -1299,8 +1268,6 @@ bool ReleaseCurrentOnThread() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Public accessors
|
||||
|
||||
bool IsInitialised() { return g_state.initialised; }
|
||||
bool HasPresentedFrame() { return g_real_swap_count.load() > 0; }
|
||||
|
||||
@@ -1670,9 +1637,7 @@ void MaybeQueueInputReadinessSequence() {
|
||||
SDL_Event focus{};
|
||||
focus.type = SDL_EVENT_WINDOW_FOCUS_GAINED;
|
||||
focus.window.windowID = window_id;
|
||||
// A non-interactive readiness runner may not receive compositor focus. Send
|
||||
// the same focus event before its opt-in click; production runs never
|
||||
// enter this path unless MOCKTAIL_INPUT_TEST_CLICK is exactly 1.
|
||||
// An opt-in readiness click may need a synthetic focus event.
|
||||
const bool needs_focus =
|
||||
(SDL_GetWindowFlags(g_state.sdl_window) & SDL_WINDOW_INPUT_FOCUS) == 0;
|
||||
|
||||
@@ -1759,8 +1724,7 @@ bool RequestFullscreenState(bool fullscreen, const char* reason) {
|
||||
const bool current_fullscreen =
|
||||
(SDL_GetWindowFlags(g_state.sdl_window) & SDL_WINDOW_FULLSCREEN) != 0;
|
||||
if (current_fullscreen != fullscreen) {
|
||||
// Capture the normal restore rectangle before SDL replaces it with the
|
||||
// monitor extent.
|
||||
// Save the restore rectangle before SDL replaces it with monitor bounds.
|
||||
CaptureWindowState();
|
||||
if (!SDL_SetWindowFullscreen(g_state.sdl_window, fullscreen)) {
|
||||
fprintf(stderr, " [fullscreen] SDL request failed: %s\n",
|
||||
|
||||
+22
-72
@@ -12,12 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// src/window/window.h — SDL3 window + real EGL context management.
|
||||
//
|
||||
// Provides a real OS window and EGL surface that Roblox can render into.
|
||||
// EGL calls from libroblox.so are intercepted by libegl_stub and forwarded
|
||||
// to the handles created here via thread-local / global state.
|
||||
|
||||
#ifndef MOCKTAIL_WINDOW_WINDOW_H_
|
||||
#define MOCKTAIL_WINDOW_WINDOW_H_
|
||||
|
||||
@@ -38,53 +32,37 @@ namespace window {
|
||||
using PreTextInputPumpCallback = bool (*)(void* context);
|
||||
using FullscreenStateSyncCallback = bool (*)(void* context, bool fullscreen);
|
||||
|
||||
// Configures the XDG-backed state record used to restore the last windowed
|
||||
// geometry, maximized state, and F11 fullscreen state. Must be called before
|
||||
// Init(); readiness runs suppress persistence internally.
|
||||
// Configures persistent geometry and fullscreen state before Init().
|
||||
Status ConfigureWindowStatePersistence(const std::filesystem::path& path);
|
||||
|
||||
// Initialises SDL3, creates the main window and the EGL display.
|
||||
// Must be called before Stage 6 (before nativeAppBridgeV2InitWithParams).
|
||||
// Returns true on success.
|
||||
bool Init(int width, int height, const char* title);
|
||||
|
||||
// Pumps pending SDL events. Call this from a main-loop or keepalive thread.
|
||||
// Returns false when the user closes the window (quit requested).
|
||||
// Returns false after the user requests quit.
|
||||
bool PumpEvents();
|
||||
|
||||
// Paces the next main-thread SDL event drain at 240 Hz. Raw mouse/touch events
|
||||
// remain unsmoothed; this only prevents the host loop from batching them at
|
||||
// the old 60 Hz cadence.
|
||||
// Paces SDL polling at 240 Hz without smoothing raw input.
|
||||
void PaceInputPump();
|
||||
|
||||
// Accepts GameActivity.setWindowFlags from any guest JNI thread and queues the
|
||||
// relevant Android fullscreen state for SDL's next main-thread event pump.
|
||||
// Returns false when the mask contains no supported fullscreen flag.
|
||||
// Queues supported GameActivity fullscreen flags for the SDL thread.
|
||||
bool RequestFullscreenFromAndroidWindowFlags(int flags, int mask);
|
||||
|
||||
// Recognizes the current Android CoreScript's otherwise-unhandled fullscreen
|
||||
// menu request and queues one toggle for the SDL thread.
|
||||
// Converts the Android fullscreen menu request into an SDL-thread toggle.
|
||||
bool RequestFullscreenFromRobloxMenuLog(const char* tag, const char* message);
|
||||
|
||||
// Registers the exact-build adapter that mirrors successful SDL fullscreen
|
||||
// requests into UserGameSettings.InFullScreen().
|
||||
// Mirrors SDL fullscreen changes into UserGameSettings.InFullScreen().
|
||||
bool SetFullscreenStateSyncCallback(FullscreenStateSyncCallback callback,
|
||||
void* context);
|
||||
void ClearFullscreenStateSyncCallback();
|
||||
|
||||
// Returns SDL-derived surface lifetime events from the same polling loop used
|
||||
// by PumpEvents. The initial mapped surface is generation 1 and is available
|
||||
// through GetWindowSurfaceSnapshot without a synthetic Created event.
|
||||
// The initial mapped surface is generation 1 and has no synthetic Created
|
||||
// event.
|
||||
bool PollWindowSurfaceEvent(WindowSurfaceEvent* event);
|
||||
WindowSurfaceSnapshot GetWindowSurfaceSnapshot();
|
||||
|
||||
// Records a compositor-observed event only after the GAME/JNI bridge has
|
||||
// committed it. The default-off resize readiness gate uses this to distinguish
|
||||
// a real rebind from an SDL configure notification that native code rejected.
|
||||
// Records an event only after the GAME/JNI bridge commits it.
|
||||
Status RecordResizeReadinessSurfaceCommit(const WindowSurfaceEvent& event);
|
||||
|
||||
// Completes the opt-in resize gate after Roblox reaches Stopped. Disabled
|
||||
// production runs return OK without adding any work to the hot path.
|
||||
// Completes the optional resize gate after Roblox reaches Stopped.
|
||||
Status StopResizeReadiness();
|
||||
Status ResizeReadinessCompletionStatus();
|
||||
|
||||
@@ -100,81 +78,60 @@ struct WindowViewportSnapshot {
|
||||
}
|
||||
};
|
||||
|
||||
// Input uses logical SDL coordinates while the native surface uses pixels.
|
||||
// Keep both extents explicit so high-DPI touch normalization cannot mix them.
|
||||
// Keep logical input and pixel surface extents separate on high-DPI displays.
|
||||
WindowViewportSnapshot GetWindowViewportSnapshot();
|
||||
|
||||
// Returns the immutable SDL display snapshot captured on the window thread.
|
||||
platform::DisplayRefreshCapabilities GetDisplayRefreshCapabilities();
|
||||
|
||||
// Registers the sole consumer of converted SDL input/focus events.
|
||||
// Surface bookkeeping is internal and does not consume this observer slot.
|
||||
// Registers the sole consumer of converted input and focus events.
|
||||
bool SetPlatformEventObserver(PlatformEventObserver observer, void* context);
|
||||
void ClearPlatformEventObserver();
|
||||
|
||||
// Applies Roblox's exact MouseBehavior.LockCenter state through SDL relative
|
||||
// mouse mode. The callback is queried only from the SDL main thread.
|
||||
// Applies MouseBehavior.LockCenter through SDL relative mouse mode.
|
||||
bool SetMouseLockQueryCallback(MouseLockQueryCallback callback, void* context);
|
||||
void ClearMouseLockQueryCallback();
|
||||
|
||||
// Registers a generic main-thread command drain that runs immediately before
|
||||
// the SDL text-input owner. Clear waits for any invocation already in flight.
|
||||
// Clear waits for a pre-input callback already in flight.
|
||||
bool SetPreTextInputPumpCallback(PreTextInputPumpCallback callback,
|
||||
void* context);
|
||||
void ClearPreTextInputPumpCallback();
|
||||
|
||||
// Guest text callbacks only enqueue state. SDL's main-thread-only text-input
|
||||
// APIs are applied by PumpEvents. The owner remains disabled until the // focused-editor bridge explicitly enables it.
|
||||
// Guest callbacks enqueue state; PumpEvents applies it on SDL's main thread.
|
||||
void SetWindowTextInputOwnerEnabled(bool enabled);
|
||||
bool RequestShowTextInput(uint64_t generation, const TextInputArea& area,
|
||||
const TextInputOptions& options);
|
||||
bool RequestHideTextInput(uint64_t generation);
|
||||
|
||||
// Returns the native EGL display handle (EGLDisplay, opaque void*).
|
||||
void* GetEGLDisplay();
|
||||
|
||||
// Returns the native EGL surface handle (EGLSurface, opaque void*) that
|
||||
// was created from the SDL3 window. This is the surface Roblox renders into.
|
||||
void* GetEGLSurface();
|
||||
|
||||
// Returns the native EGL context handle (EGLContext, opaque void*).
|
||||
void* GetEGLContext();
|
||||
|
||||
// Returns the native EGL config handle (EGLConfig, opaque void*).
|
||||
void* GetEGLConfig();
|
||||
|
||||
// Returns the raw ANativeWindow pointer used as the Android surface.
|
||||
// This is what ANativeWindow_fromSurface() should return.
|
||||
// Raw Android surface returned by ANativeWindow_fromSurface().
|
||||
void* GetNativeWindow();
|
||||
|
||||
// Returns the SDL_Window as an opaque pointer for the Vulkan WSI
|
||||
// adapter. Android guest code must continue to use GetNativeWindow().
|
||||
// Host-only SDL_Window handle for Vulkan WSI.
|
||||
void* GetBackendWindow();
|
||||
|
||||
// True when the window was created with SDL_WINDOW_VULKAN and no EGL context.
|
||||
bool UsesDirectVulkan();
|
||||
|
||||
// Resolves an OpenGL ES function from SDL's currently loaded GL library.
|
||||
// Returns nullptr before Init() or when no real GL backend is available.
|
||||
// Returns nullptr before Init() or without a GL backend.
|
||||
void* GetGLProcAddress(const char* name);
|
||||
|
||||
// Swaps front and back buffers (calls eglSwapBuffers on the real surface).
|
||||
bool SwapBuffers();
|
||||
|
||||
// Returns true once the engine has presented at least one frame.
|
||||
bool HasPresentedFrame();
|
||||
|
||||
// Records a successful host vkQueuePresentKHR. The Vulkan loader adapter is
|
||||
// the only production caller; tests must not fabricate readiness with it.
|
||||
// Records a real vkQueuePresentKHR; tests must not fabricate readiness.
|
||||
void NoteVulkanPresent();
|
||||
|
||||
// Records VK_ERROR_OUT_OF_DATE_KHR without invoking SDL or JNI on the render
|
||||
// thread. PumpEvents coalesces this evidence into surface rebinds.
|
||||
// PumpEvents coalesces render-thread out-of-date reports into surface rebinds.
|
||||
void NoteVulkanSurfaceOutOfDate();
|
||||
|
||||
// Registers one synchronous observer for successful host presentation through
|
||||
// either vkQueuePresentKHR or SDL_GL_SwapWindow. ClearPresentObserver waits for
|
||||
// an in-flight callback, allowing its context to be destroyed safely.
|
||||
// ClearPresentObserver waits for an in-flight host-present callback.
|
||||
bool SetPresentObserver(PresentObserver observer, void* context);
|
||||
void ClearPresentObserver();
|
||||
|
||||
@@ -193,24 +150,17 @@ class ScopedPresentObserver final {
|
||||
bool registered_ = false;
|
||||
};
|
||||
|
||||
// Shows the SDL window if it was created hidden for startup.
|
||||
void ShowIfHidden();
|
||||
|
||||
// Binds the EGL context to the calling thread.
|
||||
// Must be called from the engine rendering thread before any GL calls.
|
||||
// GLdispatch (Mesa) initialises TLS on the first MakeCurrent per thread.
|
||||
// The render thread must bind EGL before GLdispatch initializes its TLS.
|
||||
bool MakeCurrentOnThread();
|
||||
|
||||
// Releases the EGL context from the calling thread.
|
||||
bool ReleaseCurrentOnThread();
|
||||
|
||||
// Destroys the window and shuts down SDL3.
|
||||
void Shutdown();
|
||||
|
||||
// Returns true if Init() was called successfully.
|
||||
bool IsInitialised();
|
||||
|
||||
// Returns current window dimensions.
|
||||
int GetWidth();
|
||||
int GetHeight();
|
||||
|
||||
|
||||
+2
-3
@@ -1,6 +1,5 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
// Apache 2.0 License
|
||||
//
|
||||
// stubs/empty.cc — empty translation unit.
|
||||
// Used as a placeholder source file for stub .so targets that only need
|
||||
// to set SONAME and pull in a real system library via DT_NEEDED.
|
||||
// Placeholder for stub libraries that only set SONAME and add a system
|
||||
// dependency through DT_NEEDED.
|
||||
+23
-26
@@ -1,11 +1,8 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
// Apache 2.0 License
|
||||
//
|
||||
// stubs/libandroid_stub.cc — Stub implementations of Android NDK functions.
|
||||
//
|
||||
// libroblox.so links against libandroid.so which does not exist on Linux.
|
||||
// This stub satisfies the dynamic linker so dlopen() can load libroblox.so.
|
||||
// All functions are no-ops or return safe sentinel values.
|
||||
// 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.
|
||||
@@ -30,7 +27,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// AAssetManager / AAsset — read-only asset access
|
||||
// Read-only asset access
|
||||
|
||||
struct AAssetManager {};
|
||||
struct AAsset {
|
||||
@@ -260,7 +257,7 @@ bool MocktailUsesDirectVulkan() {
|
||||
extern "C" {
|
||||
|
||||
AAsset* AAssetManager_open(AAssetManager* mgr, const char* filename,
|
||||
int /*mode*/) {
|
||||
int) {
|
||||
if (mgr == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -418,7 +415,7 @@ int AAsset_openFileDescriptor64(AAsset* asset, off_t* outStart,
|
||||
return AAsset_openFileDescriptor(asset, outStart, outLength);
|
||||
}
|
||||
|
||||
AAssetManager* AAssetManager_fromJava(void* /*env*/, void* /*assetManager*/) {
|
||||
AAssetManager* AAssetManager_fromJava(void*, void*) {
|
||||
return &g_asset_manager;
|
||||
}
|
||||
|
||||
@@ -432,36 +429,36 @@ AConfiguration* AConfiguration_new() {
|
||||
|
||||
void AConfiguration_delete(AConfiguration* config) { free(config); }
|
||||
|
||||
void AConfiguration_fromAssetManager(AConfiguration* /*config*/,
|
||||
AAssetManager* /*am*/) {}
|
||||
void AConfiguration_fromAssetManager(AConfiguration*,
|
||||
AAssetManager*) {}
|
||||
|
||||
void AConfiguration_getLanguage(AConfiguration* /*config*/, char* out) {
|
||||
void AConfiguration_getLanguage(AConfiguration*, char* out) {
|
||||
if (out) {
|
||||
out[0] = 'e';
|
||||
out[1] = 'n';
|
||||
}
|
||||
}
|
||||
|
||||
void AConfiguration_getCountry(AConfiguration* /*config*/, char* out) {
|
||||
void AConfiguration_getCountry(AConfiguration*, char* out) {
|
||||
if (out) {
|
||||
out[0] = 'U';
|
||||
out[1] = 'S';
|
||||
}
|
||||
}
|
||||
|
||||
int32_t AConfiguration_getScreenSize(AConfiguration* /*config*/) {
|
||||
int32_t AConfiguration_getScreenSize(AConfiguration*) {
|
||||
return 2; // ACONFIGURATION_SCREENSIZE_NORMAL
|
||||
}
|
||||
|
||||
int32_t AConfiguration_getScreenWidthDp(AConfiguration* /*config*/) {
|
||||
int32_t AConfiguration_getScreenWidthDp(AConfiguration*) {
|
||||
return 360;
|
||||
}
|
||||
|
||||
int32_t AConfiguration_getScreenHeightDp(AConfiguration* /*config*/) {
|
||||
int32_t AConfiguration_getScreenHeightDp(AConfiguration*) {
|
||||
return 640;
|
||||
}
|
||||
|
||||
int32_t AConfiguration_getNavHidden(AConfiguration* /*config*/) {
|
||||
int32_t AConfiguration_getNavHidden(AConfiguration*) {
|
||||
return 1; // ACONFIGURATION_NAVHIDDEN_YES
|
||||
}
|
||||
|
||||
@@ -469,7 +466,7 @@ int32_t AConfiguration_getNavHidden(AConfiguration* /*config*/) {
|
||||
|
||||
struct ALooper {};
|
||||
|
||||
ALooper* ALooper_prepare(int /*opts*/) {
|
||||
ALooper* ALooper_prepare(int) {
|
||||
static ALooper kLooper;
|
||||
return &kLooper;
|
||||
}
|
||||
@@ -479,18 +476,18 @@ ALooper* ALooper_forThread() {
|
||||
return &kLooper;
|
||||
}
|
||||
|
||||
void ALooper_acquire(ALooper* /*looper*/) {}
|
||||
void ALooper_release(ALooper* /*looper*/) {}
|
||||
void ALooper_acquire(ALooper*) {}
|
||||
void ALooper_release(ALooper*) {}
|
||||
|
||||
int ALooper_addFd(ALooper* /*looper*/, int /*fd*/, int /*ident*/,
|
||||
int /*events*/, void* /*callback*/, void* /*data*/) {
|
||||
int ALooper_addFd(ALooper*, int, int,
|
||||
int, void*, void*) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ALooper_removeFd(ALooper* /*looper*/, int /*fd*/) { return 1; }
|
||||
int ALooper_removeFd(ALooper*, int) { return 1; }
|
||||
|
||||
int ALooper_pollOnce(int /*timeoutMillis*/, int* /*outFd*/, int* /*outEvents*/,
|
||||
void** /*outData*/) {
|
||||
int ALooper_pollOnce(int, int*, int*,
|
||||
void**) {
|
||||
return -2; // ALOOPER_POLL_TIMEOUT
|
||||
}
|
||||
|
||||
@@ -498,8 +495,8 @@ int ALooper_pollOnce(int /*timeoutMillis*/, int* /*outFd*/, int* /*outEvents*/,
|
||||
|
||||
struct ANativeWindow {};
|
||||
|
||||
void ANativeWindow_acquire(ANativeWindow* /*window*/) {}
|
||||
void ANativeWindow_release(ANativeWindow* /*window*/) {}
|
||||
void ANativeWindow_acquire(ANativeWindow*) {}
|
||||
void ANativeWindow_release(ANativeWindow*) {}
|
||||
|
||||
ANativeWindow* ANativeWindow_fromSurface(void* env, void* surface) {
|
||||
void* native_window = MocktailNativeWindow();
|
||||
|
||||
@@ -220,7 +220,7 @@ void SetError(EGLint error) {
|
||||
|
||||
extern "C" {
|
||||
|
||||
EGLDisplay eglGetDisplay(EGLNativeDisplayType /*display_id*/) {
|
||||
EGLDisplay eglGetDisplay(EGLNativeDisplayType) {
|
||||
EGLDisplay display = ActiveDisplay();
|
||||
SetError(display != nullptr ? kEglSuccess : kEglNotInitialized);
|
||||
return display;
|
||||
@@ -250,7 +250,7 @@ EGLBoolean eglBindAPI(EGLenum api) {
|
||||
return kEglTrue;
|
||||
}
|
||||
|
||||
EGLBoolean eglChooseConfig(EGLDisplay display, const EGLint* /*attrib_list*/,
|
||||
EGLBoolean eglChooseConfig(EGLDisplay display, const EGLint*,
|
||||
EGLConfig* configs, EGLint config_size,
|
||||
EGLint* num_config) {
|
||||
if (!IsActiveDisplay(display)) {
|
||||
@@ -292,7 +292,7 @@ EGLBoolean eglGetConfigAttrib(EGLDisplay display, EGLConfig config,
|
||||
|
||||
EGLSurface eglCreateWindowSurface(EGLDisplay display, EGLConfig config,
|
||||
EGLNativeWindowType native_window,
|
||||
const EGLint* /*attrib_list*/) {
|
||||
const EGLint*) {
|
||||
if (!IsActiveDisplay(display) || config == nullptr ||
|
||||
config != ActiveConfig()) {
|
||||
SetError(kEglBadDisplay);
|
||||
@@ -314,7 +314,7 @@ EGLSurface eglCreateWindowSurface(EGLDisplay display, EGLConfig config,
|
||||
}
|
||||
|
||||
EGLSurface eglCreatePbufferSurface(EGLDisplay display, EGLConfig config,
|
||||
const EGLint* /*attrib_list*/) {
|
||||
const EGLint*) {
|
||||
if (!IsActiveDisplay(display) || config == nullptr ||
|
||||
config != ActiveConfig()) {
|
||||
SetError(kEglBadDisplay);
|
||||
@@ -329,8 +329,8 @@ EGLSurface eglCreatePbufferSurface(EGLDisplay display, EGLConfig config,
|
||||
}
|
||||
|
||||
EGLContext eglCreateContext(EGLDisplay display, EGLConfig config,
|
||||
EGLContext /*share_context*/,
|
||||
const EGLint* /*attrib_list*/) {
|
||||
EGLContext,
|
||||
const EGLint*) {
|
||||
if (!IsActiveDisplay(display) || config == nullptr ||
|
||||
config != ActiveConfig()) {
|
||||
SetError(kEglBadDisplay);
|
||||
@@ -427,7 +427,7 @@ EGLBoolean eglSwapBuffers(EGLDisplay display, EGLSurface surface) {
|
||||
return swapped ? kEglTrue : kEglFalse;
|
||||
}
|
||||
|
||||
EGLBoolean eglSwapInterval(EGLDisplay display, EGLint /*interval*/) {
|
||||
EGLBoolean eglSwapInterval(EGLDisplay display, EGLint) {
|
||||
if (!IsActiveDisplay(display) || g_current_context == nullptr) {
|
||||
SetError(kEglBadDisplay);
|
||||
return kEglFalse;
|
||||
|
||||
+96
-96
@@ -176,10 +176,10 @@ void glGenRenderbuffers(GLsizei n, GLuint* renderbuffers) {
|
||||
FillObjects(n, renderbuffers);
|
||||
}
|
||||
|
||||
GLuint glCreateShader(GLenum /*type*/) { return NextObject(); }
|
||||
GLuint glCreateShader(GLenum) { return NextObject(); }
|
||||
GLuint glCreateProgram() { return NextObject(); }
|
||||
|
||||
void glGetShaderiv(GLuint /*shader*/, GLenum pname, GLint* params) {
|
||||
void glGetShaderiv(GLuint, GLenum pname, GLint* params) {
|
||||
if (params == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -196,7 +196,7 @@ void glGetShaderiv(GLuint /*shader*/, GLenum pname, GLint* params) {
|
||||
}
|
||||
}
|
||||
|
||||
void glGetProgramiv(GLuint /*program*/, GLenum pname, GLint* params) {
|
||||
void glGetProgramiv(GLuint, GLenum pname, GLint* params) {
|
||||
if (params == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -213,7 +213,7 @@ void glGetProgramiv(GLuint /*program*/, GLenum pname, GLint* params) {
|
||||
}
|
||||
}
|
||||
|
||||
void glGetShaderInfoLog(GLuint /*shader*/, GLsizei buf_size, GLsizei* length,
|
||||
void glGetShaderInfoLog(GLuint, GLsizei buf_size, GLsizei* length,
|
||||
GLchar* info_log) {
|
||||
if (!RequireTestGraphicsStub()) {
|
||||
CopyString("real GLES backend unavailable", buf_size, length, info_log);
|
||||
@@ -222,7 +222,7 @@ void glGetShaderInfoLog(GLuint /*shader*/, GLsizei buf_size, GLsizei* length,
|
||||
CopyString("", buf_size, length, info_log);
|
||||
}
|
||||
|
||||
void glGetProgramInfoLog(GLuint /*program*/, GLsizei buf_size, GLsizei* length,
|
||||
void glGetProgramInfoLog(GLuint, GLsizei buf_size, GLsizei* length,
|
||||
GLchar* info_log) {
|
||||
if (!RequireTestGraphicsStub()) {
|
||||
CopyString("real GLES backend unavailable", buf_size, length, info_log);
|
||||
@@ -231,7 +231,7 @@ void glGetProgramInfoLog(GLuint /*program*/, GLsizei buf_size, GLsizei* length,
|
||||
CopyString("", buf_size, length, info_log);
|
||||
}
|
||||
|
||||
void glGetActiveUniform(GLuint /*program*/, GLuint /*index*/, GLsizei buf_size,
|
||||
void glGetActiveUniform(GLuint, GLuint, GLsizei buf_size,
|
||||
GLsizei* length, GLint* size, GLenum* type,
|
||||
GLchar* name) {
|
||||
if (!RequireTestGraphicsStub()) {
|
||||
@@ -253,11 +253,11 @@ void glGetActiveUniform(GLuint /*program*/, GLuint /*index*/, GLsizei buf_size,
|
||||
CopyString("", buf_size, length, name);
|
||||
}
|
||||
|
||||
GLint glGetUniformLocation(GLuint /*program*/, const GLchar* /*name*/) {
|
||||
GLint glGetUniformLocation(GLuint, const GLchar*) {
|
||||
return RequireTestGraphicsStub() ? 0 : -1;
|
||||
}
|
||||
|
||||
GLenum glCheckFramebufferStatus(GLenum /*target*/) {
|
||||
GLenum glCheckFramebufferStatus(GLenum) {
|
||||
return RequireTestGraphicsStub() ? kGlFramebufferComplete
|
||||
: kGlFramebufferUnsupported;
|
||||
}
|
||||
@@ -272,119 +272,119 @@ void glViewport(GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||
g_viewport[3] = height;
|
||||
}
|
||||
|
||||
void glActiveTexture(GLenum /*texture*/) { RequireTestGraphicsStub(); }
|
||||
void glAttachShader(GLuint /*program*/, GLuint /*shader*/) {
|
||||
void glActiveTexture(GLenum) { RequireTestGraphicsStub(); }
|
||||
void glAttachShader(GLuint, GLuint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glBindAttribLocation(GLuint /*program*/, GLuint /*index*/,
|
||||
const GLchar* /*name*/) {
|
||||
void glBindAttribLocation(GLuint, GLuint,
|
||||
const GLchar*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glBindBuffer(GLenum /*target*/, GLuint /*buffer*/) {
|
||||
void glBindBuffer(GLenum, GLuint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glBindFramebuffer(GLenum /*target*/, GLuint /*framebuffer*/) {
|
||||
void glBindFramebuffer(GLenum, GLuint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glBindRenderbuffer(GLenum /*target*/, GLuint /*renderbuffer*/) {
|
||||
void glBindRenderbuffer(GLenum, GLuint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glBindTexture(GLenum /*target*/, GLuint /*texture*/) {
|
||||
void glBindTexture(GLenum, GLuint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glBlendFunc(GLenum /*sfactor*/, GLenum /*dfactor*/) {
|
||||
void glBlendFunc(GLenum, GLenum) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glBlendFuncSeparate(GLenum /*src_rgb*/, GLenum /*dst_rgb*/,
|
||||
GLenum /*src_alpha*/, GLenum /*dst_alpha*/) {
|
||||
void glBlendFuncSeparate(GLenum, GLenum,
|
||||
GLenum, GLenum) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glBufferData(GLenum /*target*/, GLsizeiptr /*size*/, const void* /*data*/,
|
||||
GLenum /*usage*/) {
|
||||
void glBufferData(GLenum, GLsizeiptr, const void*,
|
||||
GLenum) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glBufferSubData(GLenum /*target*/, GLintptr /*offset*/,
|
||||
GLsizeiptr /*size*/, const void* /*data*/) {
|
||||
void glBufferSubData(GLenum, GLintptr,
|
||||
GLsizeiptr, const void*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glClear(GLbitfield /*mask*/) { RequireTestGraphicsStub(); }
|
||||
void glClearColor(GLfloat /*red*/, GLfloat /*green*/, GLfloat /*blue*/,
|
||||
GLfloat /*alpha*/) {
|
||||
void glClear(GLbitfield) { RequireTestGraphicsStub(); }
|
||||
void glClearColor(GLfloat, GLfloat, GLfloat,
|
||||
GLfloat) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glClearDepthf(GLfloat /*depth*/) { RequireTestGraphicsStub(); }
|
||||
void glClearStencil(GLint /*s*/) { RequireTestGraphicsStub(); }
|
||||
void glColorMask(GLboolean /*red*/, GLboolean /*green*/, GLboolean /*blue*/,
|
||||
GLboolean /*alpha*/) {
|
||||
void glClearDepthf(GLfloat) { RequireTestGraphicsStub(); }
|
||||
void glClearStencil(GLint) { RequireTestGraphicsStub(); }
|
||||
void glColorMask(GLboolean, GLboolean, GLboolean,
|
||||
GLboolean) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glCompileShader(GLuint /*shader*/) { RequireTestGraphicsStub(); }
|
||||
void glCompressedTexImage2D(GLenum /*target*/, GLint /*level*/,
|
||||
GLenum /*internalformat*/, GLsizei /*width*/,
|
||||
GLsizei /*height*/, GLint /*border*/,
|
||||
GLsizei /*image_size*/, const void* /*data*/) {
|
||||
void glCompileShader(GLuint) { RequireTestGraphicsStub(); }
|
||||
void glCompressedTexImage2D(GLenum, GLint,
|
||||
GLenum, GLsizei,
|
||||
GLsizei, GLint,
|
||||
GLsizei, const void*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glCompressedTexSubImage2D(GLenum /*target*/, GLint /*level*/,
|
||||
GLint /*xoffset*/, GLint /*yoffset*/,
|
||||
GLsizei /*width*/, GLsizei /*height*/,
|
||||
GLenum /*format*/, GLsizei /*image_size*/,
|
||||
const void* /*data*/) {
|
||||
void glCompressedTexSubImage2D(GLenum, GLint,
|
||||
GLint, GLint,
|
||||
GLsizei, GLsizei,
|
||||
GLenum, GLsizei,
|
||||
const void*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glCopyTexSubImage2D(GLenum /*target*/, GLint /*level*/, GLint /*xoffset*/,
|
||||
GLint /*yoffset*/, GLint /*x*/, GLint /*y*/,
|
||||
GLsizei /*width*/, GLsizei /*height*/) {
|
||||
void glCopyTexSubImage2D(GLenum, GLint, GLint,
|
||||
GLint, GLint, GLint,
|
||||
GLsizei, GLsizei) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glCullFace(GLenum /*mode*/) { RequireTestGraphicsStub(); }
|
||||
void glDeleteBuffers(GLsizei /*n*/, const GLuint* /*buffers*/) {
|
||||
void glCullFace(GLenum) { RequireTestGraphicsStub(); }
|
||||
void glDeleteBuffers(GLsizei, const GLuint*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glDeleteFramebuffers(GLsizei /*n*/, const GLuint* /*framebuffers*/) {
|
||||
void glDeleteFramebuffers(GLsizei, const GLuint*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glDeleteProgram(GLuint /*program*/) { RequireTestGraphicsStub(); }
|
||||
void glDeleteRenderbuffers(GLsizei /*n*/, const GLuint* /*renderbuffers*/) {
|
||||
void glDeleteProgram(GLuint) { RequireTestGraphicsStub(); }
|
||||
void glDeleteRenderbuffers(GLsizei, const GLuint*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glDeleteShader(GLuint /*shader*/) { RequireTestGraphicsStub(); }
|
||||
void glDeleteTextures(GLsizei /*n*/, const GLuint* /*textures*/) {
|
||||
void glDeleteShader(GLuint) { RequireTestGraphicsStub(); }
|
||||
void glDeleteTextures(GLsizei, const GLuint*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glDepthFunc(GLenum /*func*/) { RequireTestGraphicsStub(); }
|
||||
void glDepthMask(GLboolean /*flag*/) { RequireTestGraphicsStub(); }
|
||||
void glDisable(GLenum /*cap*/) { RequireTestGraphicsStub(); }
|
||||
void glDisableVertexAttribArray(GLuint /*index*/) { RequireTestGraphicsStub(); }
|
||||
void glDrawArrays(GLenum /*mode*/, GLint /*first*/, GLsizei /*count*/) {
|
||||
void glDepthFunc(GLenum) { RequireTestGraphicsStub(); }
|
||||
void glDepthMask(GLboolean) { RequireTestGraphicsStub(); }
|
||||
void glDisable(GLenum) { RequireTestGraphicsStub(); }
|
||||
void glDisableVertexAttribArray(GLuint) { RequireTestGraphicsStub(); }
|
||||
void glDrawArrays(GLenum, GLint, GLsizei) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glDrawElements(GLenum /*mode*/, GLsizei /*count*/, GLenum /*type*/,
|
||||
const void* /*indices*/) {
|
||||
void glDrawElements(GLenum, GLsizei, GLenum,
|
||||
const void*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glEnable(GLenum /*cap*/) { RequireTestGraphicsStub(); }
|
||||
void glEnableVertexAttribArray(GLuint /*index*/) { RequireTestGraphicsStub(); }
|
||||
void glFramebufferRenderbuffer(GLenum /*target*/, GLenum /*attachment*/,
|
||||
GLenum /*renderbuffertarget*/,
|
||||
GLuint /*renderbuffer*/) {
|
||||
void glEnable(GLenum) { RequireTestGraphicsStub(); }
|
||||
void glEnableVertexAttribArray(GLuint) { RequireTestGraphicsStub(); }
|
||||
void glFramebufferRenderbuffer(GLenum, GLenum,
|
||||
GLenum,
|
||||
GLuint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glFramebufferTexture2D(GLenum /*target*/, GLenum /*attachment*/,
|
||||
GLenum /*textarget*/, GLuint /*texture*/,
|
||||
GLint /*level*/) {
|
||||
void glFramebufferTexture2D(GLenum, GLenum,
|
||||
GLenum, GLuint,
|
||||
GLint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glGenerateMipmap(GLenum /*target*/) { RequireTestGraphicsStub(); }
|
||||
void glLinkProgram(GLuint /*program*/) { RequireTestGraphicsStub(); }
|
||||
void glPixelStorei(GLenum /*pname*/, GLint /*param*/) {
|
||||
void glGenerateMipmap(GLenum) { RequireTestGraphicsStub(); }
|
||||
void glLinkProgram(GLuint) { RequireTestGraphicsStub(); }
|
||||
void glPixelStorei(GLenum, GLint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glPolygonOffset(GLfloat /*factor*/, GLfloat /*units*/) {
|
||||
void glPolygonOffset(GLfloat, GLfloat) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glReadPixels(GLint /*x*/, GLint /*y*/, GLsizei width, GLsizei height,
|
||||
GLenum /*format*/, GLenum /*type*/, void* pixels) {
|
||||
void glReadPixels(GLint, GLint, GLsizei width, GLsizei height,
|
||||
GLenum, GLenum, void* pixels) {
|
||||
if (!RequireTestGraphicsStub()) {
|
||||
return;
|
||||
}
|
||||
@@ -394,53 +394,53 @@ void glReadPixels(GLint /*x*/, GLint /*y*/, GLsizei width, GLsizei height,
|
||||
}
|
||||
}
|
||||
void glReleaseShaderCompiler() { RequireTestGraphicsStub(); }
|
||||
void glRenderbufferStorage(GLenum /*target*/, GLenum /*internalformat*/,
|
||||
GLsizei /*width*/, GLsizei /*height*/) {
|
||||
void glRenderbufferStorage(GLenum, GLenum,
|
||||
GLsizei, GLsizei) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glScissor(GLint /*x*/, GLint /*y*/, GLsizei /*width*/,
|
||||
GLsizei /*height*/) {
|
||||
void glScissor(GLint, GLint, GLsizei,
|
||||
GLsizei) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glShaderSource(GLuint /*shader*/, GLsizei /*count*/,
|
||||
const GLchar* const* /*string*/, const GLint* /*length*/) {
|
||||
void glShaderSource(GLuint, GLsizei,
|
||||
const GLchar* const*, const GLint*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glStencilFunc(GLenum /*func*/, GLint /*ref*/, GLuint /*mask*/) {
|
||||
void glStencilFunc(GLenum, GLint, GLuint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glStencilMask(GLuint /*mask*/) { RequireTestGraphicsStub(); }
|
||||
void glStencilOp(GLenum /*fail*/, GLenum /*zfail*/, GLenum /*zpass*/) {
|
||||
void glStencilMask(GLuint) { RequireTestGraphicsStub(); }
|
||||
void glStencilOp(GLenum, GLenum, GLenum) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glTexImage2D(GLenum /*target*/, GLint /*level*/, GLint /*internalformat*/,
|
||||
GLsizei /*width*/, GLsizei /*height*/, GLint /*border*/,
|
||||
GLenum /*format*/, GLenum /*type*/, const void* /*pixels*/) {
|
||||
void glTexImage2D(GLenum, GLint, GLint,
|
||||
GLsizei, GLsizei, GLint,
|
||||
GLenum, GLenum, const void*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glTexParameterf(GLenum /*target*/, GLenum /*pname*/, GLfloat /*param*/) {
|
||||
void glTexParameterf(GLenum, GLenum, GLfloat) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glTexParameterfv(GLenum /*target*/, GLenum /*pname*/,
|
||||
const GLfloat* /*params*/) {
|
||||
void glTexParameterfv(GLenum, GLenum,
|
||||
const GLfloat*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glTexParameteri(GLenum /*target*/, GLenum /*pname*/, GLint /*param*/) {
|
||||
void glTexParameteri(GLenum, GLenum, GLint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glTexSubImage2D(GLenum /*target*/, GLint /*level*/, GLint /*xoffset*/,
|
||||
GLint /*yoffset*/, GLsizei /*width*/, GLsizei /*height*/,
|
||||
GLenum /*format*/, GLenum /*type*/,
|
||||
const void* /*pixels*/) {
|
||||
void glTexSubImage2D(GLenum, GLint, GLint,
|
||||
GLint, GLsizei, GLsizei,
|
||||
GLenum, GLenum,
|
||||
const void*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glUniform1i(GLint /*location*/, GLint /*v0*/) {
|
||||
void glUniform1i(GLint, GLint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glUseProgram(GLuint /*program*/) { RequireTestGraphicsStub(); }
|
||||
void glVertexAttribPointer(GLuint /*index*/, GLint /*size*/, GLenum /*type*/,
|
||||
GLboolean /*normalized*/, GLsizei /*stride*/,
|
||||
const void* /*pointer*/) {
|
||||
void glUseProgram(GLuint) { RequireTestGraphicsStub(); }
|
||||
void glVertexAttribPointer(GLuint, GLint, GLenum,
|
||||
GLboolean, GLsizei,
|
||||
const void*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
// Apache 2.0 License
|
||||
//
|
||||
// stubs/liblog_stub.cc — Stub for Android liblog.so.
|
||||
// Redirects __android_log_print et al. to stderr on the host.
|
||||
// Redirects Android logging calls to stderr on the host.
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdarg>
|
||||
@@ -95,7 +94,7 @@ void __android_log_assert(const char* cond, const char* tag,
|
||||
__builtin_trap();
|
||||
}
|
||||
|
||||
int __android_log_buf_write(int /*bufID*/, int prio, const char* tag,
|
||||
int __android_log_buf_write(int, int prio, const char* tag,
|
||||
const char* text) {
|
||||
return __android_log_write(prio, tag, text);
|
||||
}
|
||||
|
||||
+39
-43
@@ -1,8 +1,7 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
// Apache 2.0 License
|
||||
//
|
||||
// stubs/libmediandk_stub.cc — Stub for Android libmediandk.so.
|
||||
// AMediaCodec / AMediaFormat are no-ops; Roblox uses them for video decoding.
|
||||
// Minimal libmediandk shim for video APIs imported by Roblox.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
@@ -14,7 +13,6 @@ struct AMediaFormat {
|
||||
char mime[64];
|
||||
};
|
||||
|
||||
// Forward-declared Android media status type.
|
||||
using media_status_t = int32_t;
|
||||
static constexpr media_status_t AMEDIA_OK = 0;
|
||||
static constexpr media_status_t AMEDIA_ERROR_UNKNOWN = -1;
|
||||
@@ -33,71 +31,69 @@ const char* AMEDIAFORMAT_KEY_SAMPLE_RATE = "sample-rate";
|
||||
const char* AMEDIAFORMAT_KEY_I_FRAME_INTERVAL = "i-frame-interval";
|
||||
const char* AMEDIAFORMAT_KEY_STRIDE = "stride";
|
||||
|
||||
// AMediaFormat
|
||||
AMediaFormat* AMediaFormat_new() {
|
||||
return static_cast<AMediaFormat*>(calloc(1, sizeof(AMediaFormat)));
|
||||
}
|
||||
void AMediaFormat_delete(AMediaFormat* fmt) { free(fmt); }
|
||||
bool AMediaFormat_getInt32(AMediaFormat* /*f*/, const char* /*k*/, int32_t* v) {
|
||||
bool AMediaFormat_getInt32(AMediaFormat*, const char*, int32_t* v) {
|
||||
if (v) *v = 0;
|
||||
return false;
|
||||
}
|
||||
bool AMediaFormat_getBuffer(AMediaFormat* /*f*/, const char* /*k*/,
|
||||
void** /*d*/, size_t* /*s*/) { return false; }
|
||||
void AMediaFormat_setInt32(AMediaFormat* /*f*/, const char* /*k*/,
|
||||
int32_t /*v*/) {}
|
||||
void AMediaFormat_setFloat(AMediaFormat* /*f*/, const char* /*k*/,
|
||||
float /*v*/) {}
|
||||
void AMediaFormat_setString(AMediaFormat* /*f*/, const char* k,
|
||||
bool AMediaFormat_getBuffer(AMediaFormat*, const char*,
|
||||
void**, size_t*) { return false; }
|
||||
void AMediaFormat_setInt32(AMediaFormat*, const char*,
|
||||
int32_t) {}
|
||||
void AMediaFormat_setFloat(AMediaFormat*, const char*,
|
||||
float) {}
|
||||
void AMediaFormat_setString(AMediaFormat*, const char* k,
|
||||
const char* v) {
|
||||
(void)k; (void)v;
|
||||
}
|
||||
void AMediaFormat_setBuffer(AMediaFormat* /*f*/, const char* /*k*/,
|
||||
const void* /*d*/, size_t /*s*/) {}
|
||||
const char* AMediaFormat_toString(AMediaFormat* /*f*/) { return "{}"; }
|
||||
void AMediaFormat_setBuffer(AMediaFormat*, const char*,
|
||||
const void*, size_t) {}
|
||||
const char* AMediaFormat_toString(AMediaFormat*) { return "{}"; }
|
||||
|
||||
// AMediaCodec
|
||||
AMediaCodec* AMediaCodec_createDecoderByType(const char* /*mime*/) {
|
||||
AMediaCodec* AMediaCodec_createDecoderByType(const char*) {
|
||||
return nullptr;
|
||||
}
|
||||
AMediaCodec* AMediaCodec_createEncoderByType(const char* /*mime*/) {
|
||||
AMediaCodec* AMediaCodec_createEncoderByType(const char*) {
|
||||
return nullptr;
|
||||
}
|
||||
media_status_t AMediaCodec_delete(AMediaCodec* /*codec*/) {
|
||||
media_status_t AMediaCodec_delete(AMediaCodec*) {
|
||||
return AMEDIA_OK;
|
||||
}
|
||||
media_status_t AMediaCodec_configure(AMediaCodec* /*codec*/,
|
||||
const AMediaFormat* /*format*/,
|
||||
void* /*surface*/, void* /*crypto*/,
|
||||
uint32_t /*flags*/) {
|
||||
media_status_t AMediaCodec_configure(AMediaCodec*,
|
||||
const AMediaFormat*,
|
||||
void*, void*,
|
||||
uint32_t) {
|
||||
return AMEDIA_ERROR_UNKNOWN;
|
||||
}
|
||||
media_status_t AMediaCodec_start(AMediaCodec* /*codec*/) {
|
||||
media_status_t AMediaCodec_start(AMediaCodec*) {
|
||||
return AMEDIA_ERROR_UNKNOWN;
|
||||
}
|
||||
media_status_t AMediaCodec_stop(AMediaCodec* /*codec*/) { return AMEDIA_OK; }
|
||||
media_status_t AMediaCodec_flush(AMediaCodec* /*codec*/) { return AMEDIA_OK; }
|
||||
uint8_t* AMediaCodec_getInputBuffer(AMediaCodec* /*codec*/, size_t /*idx*/,
|
||||
size_t* /*sz*/) { return nullptr; }
|
||||
uint8_t* AMediaCodec_getOutputBuffer(AMediaCodec* /*codec*/, size_t /*idx*/,
|
||||
size_t* /*sz*/) { return nullptr; }
|
||||
ssize_t AMediaCodec_dequeueInputBuffer(AMediaCodec* /*codec*/,
|
||||
int64_t /*timeoutUs*/) { return -1; }
|
||||
ssize_t AMediaCodec_dequeueOutputBuffer(AMediaCodec* /*codec*/,
|
||||
void* /*info*/,
|
||||
int64_t /*timeoutUs*/) { return -1; }
|
||||
media_status_t AMediaCodec_queueInputBuffer(AMediaCodec* /*codec*/,
|
||||
size_t /*idx*/, size_t /*offset*/,
|
||||
size_t /*size*/, uint64_t /*pts*/,
|
||||
uint32_t /*flags*/) {
|
||||
media_status_t AMediaCodec_stop(AMediaCodec*) { return AMEDIA_OK; }
|
||||
media_status_t AMediaCodec_flush(AMediaCodec*) { return AMEDIA_OK; }
|
||||
uint8_t* AMediaCodec_getInputBuffer(AMediaCodec*, size_t,
|
||||
size_t*) { return nullptr; }
|
||||
uint8_t* AMediaCodec_getOutputBuffer(AMediaCodec*, size_t,
|
||||
size_t*) { return nullptr; }
|
||||
ssize_t AMediaCodec_dequeueInputBuffer(AMediaCodec*,
|
||||
int64_t) { return -1; }
|
||||
ssize_t AMediaCodec_dequeueOutputBuffer(AMediaCodec*,
|
||||
void*,
|
||||
int64_t) { return -1; }
|
||||
media_status_t AMediaCodec_queueInputBuffer(AMediaCodec*,
|
||||
size_t, size_t,
|
||||
size_t, uint64_t,
|
||||
uint32_t) {
|
||||
return AMEDIA_ERROR_UNKNOWN;
|
||||
}
|
||||
media_status_t AMediaCodec_releaseOutputBuffer(AMediaCodec* /*codec*/,
|
||||
size_t /*idx*/,
|
||||
bool /*render*/) {
|
||||
media_status_t AMediaCodec_releaseOutputBuffer(AMediaCodec*,
|
||||
size_t,
|
||||
bool) {
|
||||
return AMEDIA_OK;
|
||||
}
|
||||
AMediaFormat* AMediaCodec_getOutputFormat(AMediaCodec* /*codec*/) {
|
||||
AMediaFormat* AMediaCodec_getOutputFormat(AMediaCodec*) {
|
||||
return AMediaFormat_new();
|
||||
}
|
||||
|
||||
|
||||
+165
-171
@@ -1,12 +1,8 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
// Apache 2.0 License
|
||||
//
|
||||
// stubs/libopensl_stub.cc — test fixture for libOpenSLES.so/libOpenMAXAL.so.
|
||||
//
|
||||
// Production audio must use the SDL-backed Mocktail audio foundation. This
|
||||
// synthetic object graph is available only when the exact opt-in
|
||||
// MOCKTAIL_ENABLE_TEST_AUDIO_STUBS=1 is present; otherwise engine creation
|
||||
// fails closed. Audio capture is unsupported in both modes.
|
||||
// Test fixture for libOpenSLES.so and libOpenMAXAL.so. Engine creation requires
|
||||
// MOCKTAIL_ENABLE_TEST_AUDIO_STUBS=1; audio capture is not implemented.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
@@ -239,37 +235,37 @@ SLresult SuccessObject(SLObjectItf* object, SLObjectItf fake_object) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
void ObjectDestroy(SLObjectItf /*self*/) {}
|
||||
void ObjectDestroy(SLObjectItf) {}
|
||||
|
||||
SLresult ObjectRealize(SLObjectItf /*self*/, SLboolean /*async*/) {
|
||||
SLresult ObjectRealize(SLObjectItf, SLboolean) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult ObjectResume(SLObjectItf /*self*/, SLboolean /*async*/) {
|
||||
SLresult ObjectResume(SLObjectItf, SLboolean) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult ObjectGetState(SLObjectItf /*self*/, SLuint32* state) {
|
||||
SLresult ObjectGetState(SLObjectItf, SLuint32* state) {
|
||||
if (state) {
|
||||
*state = SL_OBJECT_STATE_REALIZED;
|
||||
}
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult ObjectRegisterCallback(SLObjectItf /*self*/,
|
||||
SLObjectCallback /*callback*/,
|
||||
void* /*context*/) {
|
||||
SLresult ObjectRegisterCallback(SLObjectItf,
|
||||
SLObjectCallback,
|
||||
void*) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
void ObjectAbortAsyncOperation(SLObjectItf /*self*/) {}
|
||||
void ObjectAbortAsyncOperation(SLObjectItf) {}
|
||||
|
||||
SLresult ObjectSetPriority(SLObjectItf /*self*/, SLint32 /*priority*/,
|
||||
SLboolean /*preemptable*/) {
|
||||
SLresult ObjectSetPriority(SLObjectItf, SLint32,
|
||||
SLboolean) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult ObjectGetPriority(SLObjectItf /*self*/, SLint32* priority,
|
||||
SLresult ObjectGetPriority(SLObjectItf, SLint32* priority,
|
||||
SLboolean* preemptable) {
|
||||
if (priority) {
|
||||
*priority = 0;
|
||||
@@ -280,20 +276,20 @@ SLresult ObjectGetPriority(SLObjectItf /*self*/, SLint32* priority,
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult ObjectSetLossOfControlInterfaces(SLObjectItf /*self*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
SLboolean /*enabled*/) {
|
||||
SLresult ObjectSetLossOfControlInterfaces(SLObjectItf,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
SLboolean) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult ObjectGetInterface(SLObjectItf self, SLInterfaceID iid,
|
||||
void* interface_out);
|
||||
SLresult CreateUnsupportedObject(SLEngineItf /*self*/, SLObjectItf* object,
|
||||
SLuint32 /*device_id*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/);
|
||||
SLresult CreateUnsupportedObject(SLEngineItf, SLObjectItf* object,
|
||||
SLuint32,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*);
|
||||
SLresult CreateAudioPlayer(SLEngineItf self, SLObjectItf* player,
|
||||
const void* source, const void* sink,
|
||||
SLuint32 num_interfaces, const SLInterfaceID* ids,
|
||||
@@ -306,46 +302,46 @@ SLresult CreateOutputMix(SLEngineItf self, SLObjectItf* mix,
|
||||
SLuint32 num_interfaces, const SLInterfaceID* ids,
|
||||
const SLboolean* required);
|
||||
|
||||
SLresult CreateMidiPlayer(SLEngineItf /*self*/, SLObjectItf* player,
|
||||
const void* /*source*/, const void* /*sink*/,
|
||||
const void* /*bank_source*/,
|
||||
const void* /*bank_sink*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/);
|
||||
SLresult CreateListener(SLEngineItf /*self*/, SLObjectItf* listener,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/);
|
||||
SLresult Create3DGroup(SLEngineItf /*self*/, SLObjectItf* group,
|
||||
SLuint32 /*num_interfaces*/, const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/);
|
||||
SLresult CreateMetadataExtractor(SLEngineItf /*self*/, SLObjectItf* extractor,
|
||||
const void* /*source*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/);
|
||||
SLresult CreateExtensionObject(SLEngineItf /*self*/, SLObjectItf* object,
|
||||
void* /*parameters*/, SLuint32 /*object_id*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/);
|
||||
SLresult QueryNumSupportedInterfaces(SLEngineItf /*self*/,
|
||||
SLuint32 /*object_id*/, SLuint32* count);
|
||||
SLresult QuerySupportedInterfaces(SLEngineItf /*self*/, SLuint32 /*object_id*/,
|
||||
SLuint32 /*index*/, SLInterfaceID* iid);
|
||||
SLresult QueryNumSupportedExtensions(SLEngineItf /*self*/, SLuint32* count);
|
||||
SLresult QuerySupportedExtension(SLEngineItf /*self*/, SLuint32 /*index*/,
|
||||
SLresult CreateMidiPlayer(SLEngineItf, SLObjectItf* player,
|
||||
const void*, const void*,
|
||||
const void*,
|
||||
const void*,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*);
|
||||
SLresult CreateListener(SLEngineItf, SLObjectItf* listener,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*);
|
||||
SLresult Create3DGroup(SLEngineItf, SLObjectItf* group,
|
||||
SLuint32, const SLInterfaceID*,
|
||||
const SLboolean*);
|
||||
SLresult CreateMetadataExtractor(SLEngineItf, SLObjectItf* extractor,
|
||||
const void*,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*);
|
||||
SLresult CreateExtensionObject(SLEngineItf, SLObjectItf* object,
|
||||
void*, SLuint32,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*);
|
||||
SLresult QueryNumSupportedInterfaces(SLEngineItf,
|
||||
SLuint32, SLuint32* count);
|
||||
SLresult QuerySupportedInterfaces(SLEngineItf, SLuint32,
|
||||
SLuint32, SLInterfaceID* iid);
|
||||
SLresult QueryNumSupportedExtensions(SLEngineItf, SLuint32* count);
|
||||
SLresult QuerySupportedExtension(SLEngineItf, SLuint32,
|
||||
char* name, SLint32* name_length);
|
||||
SLresult IsExtensionSupported(SLEngineItf /*self*/, const char* /*name*/,
|
||||
SLresult IsExtensionSupported(SLEngineItf, const char*,
|
||||
SLboolean* supported);
|
||||
|
||||
SLresult PlaySetState(SLPlayItf /*self*/, SLuint32 state) {
|
||||
SLresult PlaySetState(SLPlayItf, SLuint32 state) {
|
||||
g_play_state = state;
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult PlayGetState(SLPlayItf /*self*/, SLuint32* state) {
|
||||
SLresult PlayGetState(SLPlayItf, SLuint32* state) {
|
||||
if (state) {
|
||||
*state = g_play_state;
|
||||
}
|
||||
@@ -359,115 +355,115 @@ SLresult ReturnZeroU32(SLuint32* value) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult PlayGetDuration(SLPlayItf /*self*/, SLuint32* duration) {
|
||||
SLresult PlayGetDuration(SLPlayItf, SLuint32* duration) {
|
||||
return ReturnZeroU32(duration);
|
||||
}
|
||||
|
||||
SLresult PlayGetPosition(SLPlayItf /*self*/, SLuint32* position) {
|
||||
SLresult PlayGetPosition(SLPlayItf, SLuint32* position) {
|
||||
return ReturnZeroU32(position);
|
||||
}
|
||||
|
||||
SLresult PlayRegisterCallback(SLPlayItf /*self*/, SLPlayCallback /*callback*/,
|
||||
void* /*context*/) {
|
||||
SLresult PlayRegisterCallback(SLPlayItf, SLPlayCallback,
|
||||
void*) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult PlaySetEventsMask(SLPlayItf /*self*/, SLuint32 /*mask*/) {
|
||||
SLresult PlaySetEventsMask(SLPlayItf, SLuint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult PlayGetEventsMask(SLPlayItf /*self*/, SLuint32* mask) {
|
||||
SLresult PlayGetEventsMask(SLPlayItf, SLuint32* mask) {
|
||||
return ReturnZeroU32(mask);
|
||||
}
|
||||
|
||||
SLresult PlaySetMarkerPosition(SLPlayItf /*self*/, SLuint32 /*position*/) {
|
||||
SLresult PlaySetMarkerPosition(SLPlayItf, SLuint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult PlayClearMarkerPosition(SLPlayItf /*self*/) {
|
||||
SLresult PlayClearMarkerPosition(SLPlayItf) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult PlayGetMarkerPosition(SLPlayItf /*self*/, SLuint32* position) {
|
||||
SLresult PlayGetMarkerPosition(SLPlayItf, SLuint32* position) {
|
||||
return ReturnZeroU32(position);
|
||||
}
|
||||
|
||||
SLresult PlaySetPositionUpdatePeriod(SLPlayItf /*self*/, SLuint32 /*period*/) {
|
||||
SLresult PlaySetPositionUpdatePeriod(SLPlayItf, SLuint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult PlayGetPositionUpdatePeriod(SLPlayItf /*self*/, SLuint32* period) {
|
||||
SLresult PlayGetPositionUpdatePeriod(SLPlayItf, SLuint32* period) {
|
||||
return ReturnZeroU32(period);
|
||||
}
|
||||
|
||||
SLresult RecordSetState(SLRecordItf /*self*/, SLuint32 state) {
|
||||
SLresult RecordSetState(SLRecordItf, SLuint32 state) {
|
||||
g_record_state = state;
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult RecordGetState(SLRecordItf /*self*/, SLuint32* state) {
|
||||
SLresult RecordGetState(SLRecordItf, SLuint32* state) {
|
||||
if (state) {
|
||||
*state = g_record_state;
|
||||
}
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult RecordSetDurationLimit(SLRecordItf /*self*/,
|
||||
SLuint32 /*milliseconds*/) {
|
||||
SLresult RecordSetDurationLimit(SLRecordItf,
|
||||
SLuint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult RecordGetPosition(SLRecordItf /*self*/, SLuint32* position) {
|
||||
SLresult RecordGetPosition(SLRecordItf, SLuint32* position) {
|
||||
return ReturnZeroU32(position);
|
||||
}
|
||||
|
||||
SLresult RecordRegisterCallback(SLRecordItf /*self*/,
|
||||
SLRecordCallback /*callback*/,
|
||||
void* /*context*/) {
|
||||
SLresult RecordRegisterCallback(SLRecordItf,
|
||||
SLRecordCallback,
|
||||
void*) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult RecordSetEventsMask(SLRecordItf /*self*/, SLuint32 /*mask*/) {
|
||||
SLresult RecordSetEventsMask(SLRecordItf, SLuint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult RecordGetEventsMask(SLRecordItf /*self*/, SLuint32* mask) {
|
||||
SLresult RecordGetEventsMask(SLRecordItf, SLuint32* mask) {
|
||||
return ReturnZeroU32(mask);
|
||||
}
|
||||
|
||||
SLresult RecordSetMarkerPosition(SLRecordItf /*self*/, SLuint32 /*position*/) {
|
||||
SLresult RecordSetMarkerPosition(SLRecordItf, SLuint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult RecordClearMarkerPosition(SLRecordItf /*self*/) {
|
||||
SLresult RecordClearMarkerPosition(SLRecordItf) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult RecordGetMarkerPosition(SLRecordItf /*self*/, SLuint32* position) {
|
||||
SLresult RecordGetMarkerPosition(SLRecordItf, SLuint32* position) {
|
||||
return ReturnZeroU32(position);
|
||||
}
|
||||
|
||||
SLresult RecordSetPositionUpdatePeriod(SLRecordItf /*self*/,
|
||||
SLuint32 /*period*/) {
|
||||
SLresult RecordSetPositionUpdatePeriod(SLRecordItf,
|
||||
SLuint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult RecordGetPositionUpdatePeriod(SLRecordItf /*self*/, SLuint32* period) {
|
||||
SLresult RecordGetPositionUpdatePeriod(SLRecordItf, SLuint32* period) {
|
||||
return ReturnZeroU32(period);
|
||||
}
|
||||
|
||||
SLresult BufferQueueEnqueue(SLBufferQueueItf /*self*/, const void* /*buffer*/,
|
||||
SLuint32 /*size*/) {
|
||||
SLresult BufferQueueEnqueue(SLBufferQueueItf, const void*,
|
||||
SLuint32) {
|
||||
++g_buffer_queue_state.count;
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult BufferQueueClear(SLBufferQueueItf /*self*/) {
|
||||
SLresult BufferQueueClear(SLBufferQueueItf) {
|
||||
g_buffer_queue_state = {};
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult BufferQueueGetState(SLBufferQueueItf /*self*/,
|
||||
SLresult BufferQueueGetState(SLBufferQueueItf,
|
||||
SLBufferQueueState* state) {
|
||||
if (state) {
|
||||
*state = g_buffer_queue_state;
|
||||
@@ -475,7 +471,7 @@ SLresult BufferQueueGetState(SLBufferQueueItf /*self*/,
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult BufferQueueRegisterCallback(SLBufferQueueItf /*self*/,
|
||||
SLresult BufferQueueRegisterCallback(SLBufferQueueItf,
|
||||
SLBufferQueueCallback callback,
|
||||
void* context) {
|
||||
g_buffer_queue_callback = callback;
|
||||
@@ -483,18 +479,18 @@ SLresult BufferQueueRegisterCallback(SLBufferQueueItf /*self*/,
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult SimpleBufferQueueEnqueue(SLAndroidSimpleBufferQueueItf /*self*/,
|
||||
const void* /*buffer*/, SLuint32 /*size*/) {
|
||||
SLresult SimpleBufferQueueEnqueue(SLAndroidSimpleBufferQueueItf,
|
||||
const void*, SLuint32) {
|
||||
++g_simple_queue_state.count;
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult SimpleBufferQueueClear(SLAndroidSimpleBufferQueueItf /*self*/) {
|
||||
SLresult SimpleBufferQueueClear(SLAndroidSimpleBufferQueueItf) {
|
||||
g_simple_queue_state = {};
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult SimpleBufferQueueGetState(SLAndroidSimpleBufferQueueItf /*self*/,
|
||||
SLresult SimpleBufferQueueGetState(SLAndroidSimpleBufferQueueItf,
|
||||
SLBufferQueueState* state) {
|
||||
if (state) {
|
||||
*state = g_simple_queue_state;
|
||||
@@ -503,40 +499,40 @@ SLresult SimpleBufferQueueGetState(SLAndroidSimpleBufferQueueItf /*self*/,
|
||||
}
|
||||
|
||||
SLresult SimpleBufferQueueRegisterCallback(
|
||||
SLAndroidSimpleBufferQueueItf /*self*/,
|
||||
SLAndroidSimpleBufferQueueItf,
|
||||
SLAndroidSimpleBufferQueueCallback callback, void* context) {
|
||||
g_simple_queue_callback = callback;
|
||||
g_simple_queue_context = context;
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult VolumeSetLevel(SLVolumeItf /*self*/, SLint32 /*level*/) {
|
||||
SLresult VolumeSetLevel(SLVolumeItf, SLint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult VolumeGetLevel(SLVolumeItf /*self*/, SLint32* level) {
|
||||
SLresult VolumeGetLevel(SLVolumeItf, SLint32* level) {
|
||||
if (level) {
|
||||
*level = 0;
|
||||
}
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult VolumeSetMute(SLVolumeItf /*self*/, SLboolean /*mute*/) {
|
||||
SLresult VolumeSetMute(SLVolumeItf, SLboolean) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult VolumeGetMute(SLVolumeItf /*self*/, SLboolean* mute) {
|
||||
SLresult VolumeGetMute(SLVolumeItf, SLboolean* mute) {
|
||||
if (mute) {
|
||||
*mute = 0;
|
||||
}
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult VolumeEnableStereoPosition(SLVolumeItf /*self*/, SLboolean /*enable*/) {
|
||||
SLresult VolumeEnableStereoPosition(SLVolumeItf, SLboolean) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult VolumeIsStereoPositionEnabled(SLVolumeItf /*self*/,
|
||||
SLresult VolumeIsStereoPositionEnabled(SLVolumeItf,
|
||||
SLboolean* enabled) {
|
||||
if (enabled) {
|
||||
*enabled = 0;
|
||||
@@ -544,23 +540,23 @@ SLresult VolumeIsStereoPositionEnabled(SLVolumeItf /*self*/,
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult VolumeSetStereoPosition(SLVolumeItf /*self*/, SLint32 /*position*/) {
|
||||
SLresult VolumeSetStereoPosition(SLVolumeItf, SLint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult VolumeGetStereoPosition(SLVolumeItf /*self*/, SLint32* position) {
|
||||
SLresult VolumeGetStereoPosition(SLVolumeItf, SLint32* position) {
|
||||
if (position) {
|
||||
*position = 0;
|
||||
}
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult ConfigSet(SLAndroidConfigurationItf /*self*/, const char* /*key*/,
|
||||
const void* /*value*/, SLuint32 /*value_size*/) {
|
||||
SLresult ConfigSet(SLAndroidConfigurationItf, const char*,
|
||||
const void*, SLuint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult ConfigGet(SLAndroidConfigurationItf /*self*/, const char* /*key*/,
|
||||
SLresult ConfigGet(SLAndroidConfigurationItf, const char*,
|
||||
SLuint32* value_size, void* value) {
|
||||
if (value_size) {
|
||||
if (value && *value_size > 0) {
|
||||
@@ -709,7 +705,7 @@ void* SL_IID_VOLUME = reinterpret_cast<void*>(0x1007);
|
||||
|
||||
namespace {
|
||||
|
||||
SLresult ObjectGetInterface(SLObjectItf /*self*/, SLInterfaceID iid,
|
||||
SLresult ObjectGetInterface(SLObjectItf, SLInterfaceID iid,
|
||||
void* interface_out) {
|
||||
if (!interface_out) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
@@ -749,107 +745,107 @@ SLresult ObjectGetInterface(SLObjectItf /*self*/, SLInterfaceID iid,
|
||||
return SL_RESULT_FEATURE_UNSUPPORTED;
|
||||
}
|
||||
|
||||
SLresult CreateUnsupportedObject(SLEngineItf /*self*/, SLObjectItf* object,
|
||||
SLuint32 /*device_id*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/) {
|
||||
SLresult CreateUnsupportedObject(SLEngineItf, SLObjectItf* object,
|
||||
SLuint32,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*) {
|
||||
return SuccessObject(object, GenericObject());
|
||||
}
|
||||
|
||||
SLresult CreateAudioPlayer(SLEngineItf /*self*/, SLObjectItf* player,
|
||||
const void* /*source*/, const void* /*sink*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/) {
|
||||
SLresult CreateAudioPlayer(SLEngineItf, SLObjectItf* player,
|
||||
const void*, const void*,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*) {
|
||||
g_play_state = SL_PLAYSTATE_STOPPED;
|
||||
g_buffer_queue_state = {};
|
||||
g_simple_queue_state = {};
|
||||
return SuccessObject(player, PlayerObject());
|
||||
}
|
||||
|
||||
SLresult CreateAudioRecorder(SLEngineItf /*self*/, SLObjectItf* recorder,
|
||||
const void* /*source*/, const void* /*sink*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/) {
|
||||
SLresult CreateAudioRecorder(SLEngineItf, SLObjectItf* recorder,
|
||||
const void*, const void*,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*) {
|
||||
if (recorder != nullptr) {
|
||||
*recorder = nullptr;
|
||||
}
|
||||
return SL_RESULT_FEATURE_UNSUPPORTED;
|
||||
}
|
||||
|
||||
SLresult CreateMidiPlayer(SLEngineItf /*self*/, SLObjectItf* player,
|
||||
const void* /*source*/, const void* /*sink*/,
|
||||
const void* /*bank_source*/,
|
||||
const void* /*bank_sink*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/) {
|
||||
SLresult CreateMidiPlayer(SLEngineItf, SLObjectItf* player,
|
||||
const void*, const void*,
|
||||
const void*,
|
||||
const void*,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*) {
|
||||
return SuccessObject(player, PlayerObject());
|
||||
}
|
||||
|
||||
SLresult CreateListener(SLEngineItf /*self*/, SLObjectItf* listener,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/) {
|
||||
SLresult CreateListener(SLEngineItf, SLObjectItf* listener,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*) {
|
||||
return SuccessObject(listener, GenericObject());
|
||||
}
|
||||
|
||||
SLresult Create3DGroup(SLEngineItf /*self*/, SLObjectItf* group,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/) {
|
||||
SLresult Create3DGroup(SLEngineItf, SLObjectItf* group,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*) {
|
||||
return SuccessObject(group, GenericObject());
|
||||
}
|
||||
|
||||
SLresult CreateOutputMix(SLEngineItf /*self*/, SLObjectItf* mix,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/) {
|
||||
SLresult CreateOutputMix(SLEngineItf, SLObjectItf* mix,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*) {
|
||||
return SuccessObject(mix, OutputMixObject());
|
||||
}
|
||||
|
||||
SLresult CreateMetadataExtractor(SLEngineItf /*self*/, SLObjectItf* extractor,
|
||||
const void* /*source*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/) {
|
||||
SLresult CreateMetadataExtractor(SLEngineItf, SLObjectItf* extractor,
|
||||
const void*,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*) {
|
||||
return SuccessObject(extractor, GenericObject());
|
||||
}
|
||||
|
||||
SLresult CreateExtensionObject(SLEngineItf /*self*/, SLObjectItf* object,
|
||||
void* /*parameters*/, SLuint32 /*object_id*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/) {
|
||||
SLresult CreateExtensionObject(SLEngineItf, SLObjectItf* object,
|
||||
void*, SLuint32,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*) {
|
||||
return SuccessObject(object, GenericObject());
|
||||
}
|
||||
|
||||
SLresult QueryNumSupportedInterfaces(SLEngineItf /*self*/,
|
||||
SLuint32 /*object_id*/, SLuint32* count) {
|
||||
SLresult QueryNumSupportedInterfaces(SLEngineItf,
|
||||
SLuint32, SLuint32* count) {
|
||||
if (count) {
|
||||
*count = 0;
|
||||
}
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult QuerySupportedInterfaces(SLEngineItf /*self*/, SLuint32 /*object_id*/,
|
||||
SLuint32 /*index*/, SLInterfaceID* iid) {
|
||||
SLresult QuerySupportedInterfaces(SLEngineItf, SLuint32,
|
||||
SLuint32, SLInterfaceID* iid) {
|
||||
if (iid) {
|
||||
*iid = nullptr;
|
||||
}
|
||||
return SL_RESULT_FEATURE_UNSUPPORTED;
|
||||
}
|
||||
|
||||
SLresult QueryNumSupportedExtensions(SLEngineItf /*self*/, SLuint32* count) {
|
||||
SLresult QueryNumSupportedExtensions(SLEngineItf, SLuint32* count) {
|
||||
if (count) {
|
||||
*count = 0;
|
||||
}
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult QuerySupportedExtension(SLEngineItf /*self*/, SLuint32 /*index*/,
|
||||
SLresult QuerySupportedExtension(SLEngineItf, SLuint32,
|
||||
char* name, SLint32* name_length) {
|
||||
if (name_length) {
|
||||
*name_length = 0;
|
||||
@@ -860,7 +856,7 @@ SLresult QuerySupportedExtension(SLEngineItf /*self*/, SLuint32 /*index*/,
|
||||
return SL_RESULT_FEATURE_UNSUPPORTED;
|
||||
}
|
||||
|
||||
SLresult IsExtensionSupported(SLEngineItf /*self*/, const char* /*name*/,
|
||||
SLresult IsExtensionSupported(SLEngineItf, const char*,
|
||||
SLboolean* supported) {
|
||||
if (supported) {
|
||||
*supported = 0;
|
||||
@@ -872,12 +868,11 @@ SLresult IsExtensionSupported(SLEngineItf /*self*/, const char* /*name*/,
|
||||
|
||||
extern "C" {
|
||||
|
||||
// Test-only OpenSL ES entry point. Production fails closed.
|
||||
SLresult slCreateEngine(void** pEngine, uint32_t /*numOptions*/,
|
||||
const void* /*pEngineOptions*/,
|
||||
uint32_t /*numInterfaces*/,
|
||||
const void* /*pInterfaceIds*/,
|
||||
const void* /*pInterfaceRequired*/) {
|
||||
SLresult slCreateEngine(void** pEngine, uint32_t,
|
||||
const void*,
|
||||
uint32_t,
|
||||
const void*,
|
||||
const void*) {
|
||||
if (pEngine == nullptr) {
|
||||
return SL_RESULT_PARAMETER_INVALID;
|
||||
}
|
||||
@@ -890,12 +885,11 @@ SLresult slCreateEngine(void** pEngine, uint32_t /*numOptions*/,
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
// Test-only OpenMAX AL entry point. Production fails closed.
|
||||
SLresult alCreateEngine(void** pEngine, uint32_t /*numOptions*/,
|
||||
const void* /*pEngineOptions*/,
|
||||
uint32_t /*numInterfaces*/,
|
||||
const void* /*pInterfaceIds*/,
|
||||
const void* /*pInterfaceRequired*/) {
|
||||
SLresult alCreateEngine(void** pEngine, uint32_t,
|
||||
const void*,
|
||||
uint32_t,
|
||||
const void*,
|
||||
const void*) {
|
||||
if (pEngine == nullptr) {
|
||||
return SL_RESULT_PARAMETER_INVALID;
|
||||
}
|
||||
|
||||
+15
-15
@@ -35,8 +35,8 @@ bool TestVulkanStubEnabled() {
|
||||
|
||||
extern "C" {
|
||||
|
||||
VkResult vkCreateInstance(const void* /*create_info*/,
|
||||
const VkAllocationCallbacks* /*allocator*/,
|
||||
VkResult vkCreateInstance(const void*,
|
||||
const VkAllocationCallbacks*,
|
||||
VkInstance* instance) {
|
||||
if (!TestVulkanStubEnabled()) {
|
||||
if (instance != nullptr) {
|
||||
@@ -50,12 +50,12 @@ VkResult vkCreateInstance(const void* /*create_info*/,
|
||||
return kVkSuccess;
|
||||
}
|
||||
|
||||
void vkDestroyInstance(VkInstance /*instance*/,
|
||||
const VkAllocationCallbacks* /*allocator*/) {}
|
||||
void vkDestroyInstance(VkInstance,
|
||||
const VkAllocationCallbacks*) {}
|
||||
|
||||
VkResult vkCreateAndroidSurfaceKHR(VkInstance /*instance*/,
|
||||
const void* /*create_info*/,
|
||||
const VkAllocationCallbacks* /*allocator*/,
|
||||
VkResult vkCreateAndroidSurfaceKHR(VkInstance,
|
||||
const void*,
|
||||
const VkAllocationCallbacks*,
|
||||
VkSurfaceKHR* surface) {
|
||||
if (!TestVulkanStubEnabled()) {
|
||||
if (surface != nullptr) {
|
||||
@@ -69,12 +69,12 @@ VkResult vkCreateAndroidSurfaceKHR(VkInstance /*instance*/,
|
||||
return kVkSuccess;
|
||||
}
|
||||
|
||||
void vkDestroySurfaceKHR(VkInstance /*instance*/, VkSurfaceKHR /*surface*/,
|
||||
const VkAllocationCallbacks* /*allocator*/) {}
|
||||
void vkDestroySurfaceKHR(VkInstance, VkSurfaceKHR,
|
||||
const VkAllocationCallbacks*) {}
|
||||
|
||||
VkResult vkEnumerateInstanceExtensionProperties(const char* /*layer_name*/,
|
||||
VkResult vkEnumerateInstanceExtensionProperties(const char*,
|
||||
uint32_t* property_count,
|
||||
void* /*properties*/) {
|
||||
void*) {
|
||||
if (property_count != nullptr) {
|
||||
*property_count = 0;
|
||||
}
|
||||
@@ -82,19 +82,19 @@ VkResult vkEnumerateInstanceExtensionProperties(const char* /*layer_name*/,
|
||||
}
|
||||
|
||||
VkResult vkEnumerateInstanceLayerProperties(uint32_t* property_count,
|
||||
void* /*properties*/) {
|
||||
void*) {
|
||||
if (property_count != nullptr) {
|
||||
*property_count = 0;
|
||||
}
|
||||
return kVkSuccess;
|
||||
}
|
||||
|
||||
PFN_vkVoidFunction vkGetDeviceProcAddr(VkDevice /*device*/,
|
||||
const char* /*name*/) {
|
||||
PFN_vkVoidFunction vkGetDeviceProcAddr(VkDevice,
|
||||
const char*) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PFN_vkVoidFunction vkGetInstanceProcAddr(VkInstance /*instance*/,
|
||||
PFN_vkVoidFunction vkGetInstanceProcAddr(VkInstance,
|
||||
const char* name) {
|
||||
if (name == nullptr) {
|
||||
return nullptr;
|
||||
|
||||
@@ -168,7 +168,7 @@ struct CallbackProbe {
|
||||
int calls = 0;
|
||||
};
|
||||
|
||||
void OpenSlQueueCallback(opensl_abi::AndroidSimpleBufferQueue /*queue*/,
|
||||
void OpenSlQueueCallback(opensl_abi::AndroidSimpleBufferQueue,
|
||||
void* context) {
|
||||
auto* probe = static_cast<CallbackProbe*>(context);
|
||||
{
|
||||
@@ -193,7 +193,7 @@ struct BlockingCallbackProbe {
|
||||
bool release = false;
|
||||
};
|
||||
|
||||
void BlockingOpenSlQueueCallback(opensl_abi::AndroidSimpleBufferQueue /*queue*/,
|
||||
void BlockingOpenSlQueueCallback(opensl_abi::AndroidSimpleBufferQueue,
|
||||
void* context) {
|
||||
auto* probe = static_cast<BlockingCallbackProbe*>(context);
|
||||
std::unique_lock<std::mutex> lock(probe->mutex);
|
||||
|
||||
@@ -981,9 +981,8 @@ rm -f -- "${FAKE_CALLS}"
|
||||
"8888-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" ]]
|
||||
export FAKE_REMOTE_VERSION=9999
|
||||
|
||||
# A candidate staged while experimental must be reconsidered after its exact
|
||||
# Build-ID profile is approved. It proceeds directly to canary without a
|
||||
# second provider download and is still promoted only after smoke succeeds.
|
||||
# Reconsider a staged candidate after its exact Build-ID profile is approved,
|
||||
# without downloading the same payload again.
|
||||
fetch_calls_before_approval="$(wc -l < "${FAKE_FETCH_CALLS}")"
|
||||
jq '(.profiles[] | select(.version_code == 9999)) |=
|
||||
(.status = "supported" | .default_allowed = true)' \
|
||||
|
||||
+1
-1
@@ -15,4 +15,4 @@ __attribute__((constructor)) void FailIfLoaded() {
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int JNI_OnLoad(void* /*vm*/, void* /*reserved*/) { return 0x10006; }
|
||||
extern "C" int JNI_OnLoad(void*, void*) { return 0x10006; }
|
||||
+2
-2
@@ -10,13 +10,13 @@
|
||||
|
||||
namespace {
|
||||
|
||||
jint NativeIncrement(JNIEnv* /*env*/, jclass /*clazz*/, jint value) {
|
||||
jint NativeIncrement(JNIEnv*, jclass, jint value) {
|
||||
return value + 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* /*reserved*/) {
|
||||
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
|
||||
if (vm == nullptr) {
|
||||
return JNI_ERR;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ class ControlledSink final : public AudioSink {
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status SetGain(float /*linear_gain*/) override { return Status::Ok(); }
|
||||
Status SetGain(float) override { return Status::Ok(); }
|
||||
Status Flush() override { return Status::Ok(); }
|
||||
|
||||
Status Clear() override {
|
||||
|
||||
+3
-38
@@ -12,14 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// tests/jnivm_test.cc — Unit tests for the Pseudo-JVM component.
|
||||
//
|
||||
// Tests cover:
|
||||
// 1. VM construction and JavaVM pointer validity.
|
||||
// 2. Class registration and deduplication.
|
||||
// 3. Method registration and lookup.
|
||||
// 4. Method callback invocation.
|
||||
|
||||
#include "jnivm/jnivm.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
@@ -38,7 +30,6 @@
|
||||
namespace jnivm {
|
||||
namespace {
|
||||
|
||||
// Fixture: provides a freshly constructed VM instance for each test.
|
||||
class JniVmTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
@@ -384,14 +375,10 @@ jobject MakeTextBoxInfo(JNIEnv *env) {
|
||||
return info;
|
||||
}
|
||||
|
||||
// VM construction
|
||||
|
||||
// A newly constructed VM must expose a non-null JavaVM pointer.
|
||||
TEST_F(JniVmTest, JavaVmPointerIsNotNull) {
|
||||
EXPECT_NE(vm_->GetJavaVM(), nullptr);
|
||||
}
|
||||
|
||||
// A newly constructed VM must expose a non-null JNIEnv pointer.
|
||||
TEST_F(JniVmTest, JniEnvPointerIsNotNull) {
|
||||
EXPECT_NE(vm_->GetJNIEnv(), nullptr);
|
||||
}
|
||||
@@ -422,20 +409,15 @@ TEST_F(JniVmTest, TraceAllNeverPrintsJStringContentOrPrefix) {
|
||||
EXPECT_EQ(trace_output.find("SECRET_CANARY_PREFIX"), std::string::npos);
|
||||
}
|
||||
|
||||
// A newly constructed VM has zero registered classes.
|
||||
TEST_F(JniVmTest, InitialClassCountIsZero) {
|
||||
EXPECT_EQ(vm_->GetClassCount(), 0u);
|
||||
}
|
||||
|
||||
// Class registration
|
||||
|
||||
// Registering a class returns a non-null shared_ptr.
|
||||
TEST_F(JniVmTest, RegisterClassReturnsNonNull) {
|
||||
auto cls = vm_->RegisterClass("android/content/Context");
|
||||
EXPECT_NE(cls, nullptr);
|
||||
}
|
||||
|
||||
// The class name is preserved exactly as provided.
|
||||
TEST_F(JniVmTest, RegisterClassPreservesName) {
|
||||
const std::string kName = "com/roblox/client/RobloxActivity";
|
||||
auto cls = vm_->RegisterClass(kName);
|
||||
@@ -443,44 +425,34 @@ TEST_F(JniVmTest, RegisterClassPreservesName) {
|
||||
EXPECT_EQ(cls->GetName(), kName);
|
||||
}
|
||||
|
||||
// Registering the same class twice returns the same instance (deduplication).
|
||||
TEST_F(JniVmTest, RegisterClassIsDeduplicated) {
|
||||
auto cls1 = vm_->RegisterClass("rbx/JNIRobloxSettings");
|
||||
auto cls2 = vm_->RegisterClass("rbx/JNIRobloxSettings");
|
||||
EXPECT_EQ(cls1.get(), cls2.get());
|
||||
}
|
||||
|
||||
// Class count increments correctly with distinct registrations.
|
||||
TEST_F(JniVmTest, ClassCountIncrements) {
|
||||
vm_->RegisterClass("android/content/Context");
|
||||
vm_->RegisterClass("android/app/Activity");
|
||||
EXPECT_EQ(vm_->GetClassCount(), 2u);
|
||||
}
|
||||
|
||||
// Registering the same class multiple times does not inflate the count.
|
||||
TEST_F(JniVmTest, DuplicateRegistrationDoesNotInflateCount) {
|
||||
vm_->RegisterClass("android/content/Context");
|
||||
vm_->RegisterClass("android/content/Context");
|
||||
EXPECT_EQ(vm_->GetClassCount(), 1u);
|
||||
}
|
||||
|
||||
// Class lookup (FindClass)
|
||||
|
||||
// FindClass returns nullptr for an unregistered class.
|
||||
TEST_F(JniVmTest, FindClassReturnsNullForUnknownClass) {
|
||||
EXPECT_EQ(vm_->FindClass("not/Registered"), nullptr);
|
||||
}
|
||||
|
||||
// FindClass returns the correct instance for a registered class.
|
||||
TEST_F(JniVmTest, FindClassReturnsRegisteredInstance) {
|
||||
auto registered = vm_->RegisterClass("android/os/Bundle");
|
||||
auto found = vm_->FindClass("android/os/Bundle");
|
||||
EXPECT_EQ(registered.get(), found.get());
|
||||
}
|
||||
|
||||
// Method registration and lookup
|
||||
|
||||
// A method registered on a class can be looked up by name + signature.
|
||||
TEST_F(JniVmTest, RegisteredMethodIsFound) {
|
||||
auto cls = vm_->RegisterClass("rbx/JNIRobloxSettings");
|
||||
cls->RegisterMethod("nativeInitClientSettings", "()V",
|
||||
@@ -488,7 +460,6 @@ TEST_F(JniVmTest, RegisteredMethodIsFound) {
|
||||
EXPECT_NE(cls->FindMethod("nativeInitClientSettings", "()V"), nullptr);
|
||||
}
|
||||
|
||||
// Looking up a method with a wrong signature returns nullptr.
|
||||
TEST_F(JniVmTest, MethodLookupWithWrongSignatureReturnsNull) {
|
||||
auto cls = vm_->RegisterClass("rbx/JNIRobloxSettings");
|
||||
cls->RegisterMethod("nativeInitClientSettings", "()V",
|
||||
@@ -496,7 +467,6 @@ TEST_F(JniVmTest, MethodLookupWithWrongSignatureReturnsNull) {
|
||||
EXPECT_EQ(cls->FindMethod("nativeInitClientSettings", "(I)V"), nullptr);
|
||||
}
|
||||
|
||||
// Looking up a non-existent method returns nullptr.
|
||||
TEST_F(JniVmTest, LookupOfNonExistentMethodReturnsNull) {
|
||||
auto cls = vm_->RegisterClass("android/content/Context");
|
||||
EXPECT_EQ(cls->FindMethod("getSystemService",
|
||||
@@ -504,9 +474,6 @@ TEST_F(JniVmTest, LookupOfNonExistentMethodReturnsNull) {
|
||||
nullptr);
|
||||
}
|
||||
|
||||
// Method callback invocation
|
||||
|
||||
// The registered callback is actually invoked when called.
|
||||
TEST_F(JniVmTest, MethodCallbackIsInvoked) {
|
||||
auto cls = vm_->RegisterClass("rbx/JNIRobloxSettings");
|
||||
bool invoked = false;
|
||||
@@ -520,7 +487,6 @@ TEST_F(JniVmTest, MethodCallbackIsInvoked) {
|
||||
EXPECT_TRUE(invoked);
|
||||
}
|
||||
|
||||
// The callback receives the correct JNIEnv pointer.
|
||||
TEST_F(JniVmTest, CallbackReceivesCorrectJniEnv) {
|
||||
auto cls = vm_->RegisterClass("android/content/Context");
|
||||
JNIEnv *received_env = nullptr;
|
||||
@@ -678,7 +644,7 @@ TEST_F(JniVmTest, NativeUserJavaInterfaceReturnsInjectedIdentityExactly) {
|
||||
identity.display_name = "Display Name";
|
||||
vm_->SetRobloxAuthIdentity(identity);
|
||||
|
||||
// The identity has priority over research environment fallbacks.
|
||||
// Injected identity takes precedence over environment fallbacks.
|
||||
setenv("MOCKTAIL_ROBLOX_USER_ID", "77", 1);
|
||||
setenv("MOCKTAIL_ROBLOX_USERNAME", "EnvironmentName", 1);
|
||||
|
||||
@@ -1543,9 +1509,8 @@ TEST_F(JniVmTest, GameActivityDispatchesTypedAndroidWindowFlags) {
|
||||
EXPECT_EQ(probe->flags, 0x800);
|
||||
EXPECT_EQ(probe->mask, 0x800);
|
||||
|
||||
// Production LuaApp retains the concrete Android object behind the
|
||||
// android.app.Activity interface. The inherited GameActivity method ID must
|
||||
// still reach the same host boundary.
|
||||
// LuaApp passes the concrete object as android.app.Activity. The inherited
|
||||
// method ID must still dispatch to the host callback.
|
||||
env->CallVoidMethod(activity_interface, set_flags, 0x400, 0x400);
|
||||
EXPECT_EQ(probe->calls, 4);
|
||||
EXPECT_EQ(probe->flags, 0x400);
|
||||
|
||||
+1
-25
@@ -12,13 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// tests/libc_shim_test.cc — Unit tests for the Bionic → Glibc path shim.
|
||||
//
|
||||
// Tests cover:
|
||||
// 1. Path translation with registered prefixes.
|
||||
// 2. Unchanged return for unregistered paths.
|
||||
// 3. Multiple prefix registrations (longest-prefix matching).
|
||||
|
||||
#include "libc_shim/libc_shim.h"
|
||||
|
||||
#include <atomic>
|
||||
@@ -39,7 +32,6 @@ void* CountingGuestAllocator(size_t size) {
|
||||
return std::malloc(size);
|
||||
}
|
||||
|
||||
// Fixture: clears all path mappings before and after each test.
|
||||
class LibcShimTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
@@ -79,9 +71,6 @@ class LibcShimTest : public ::testing::Test {
|
||||
std::string original_asset_path_override_;
|
||||
};
|
||||
|
||||
// TranslatePath — basic mapping
|
||||
|
||||
// An Android path with a registered prefix is correctly translated.
|
||||
TEST_F(LibcShimTest, TranslatesRegisteredPrefix) {
|
||||
RegisterPathMapping("/data/user/0/com.roblox.client",
|
||||
"/home/user/.var/app/org.vinegarhq.Sober/data/sober");
|
||||
@@ -93,7 +82,6 @@ TEST_F(LibcShimTest, TranslatesRegisteredPrefix) {
|
||||
EXPECT_EQ(TranslatePath(input), expected);
|
||||
}
|
||||
|
||||
// A path with no registered prefix is returned unchanged.
|
||||
TEST_F(LibcShimTest, UnregisteredPathIsReturnedUnchanged) {
|
||||
const std::string input = "/proc/self/maps";
|
||||
EXPECT_EQ(TranslatePath(input), input);
|
||||
@@ -111,14 +99,10 @@ TEST_F(LibcShimTest, TranslatesRelativeRobloxAssetPrefixes) {
|
||||
"/payload/assets/shaders/mobile.pack");
|
||||
}
|
||||
|
||||
// An empty path is returned unchanged.
|
||||
TEST_F(LibcShimTest, EmptyPathIsReturnedUnchanged) {
|
||||
EXPECT_EQ(TranslatePath(""), "");
|
||||
}
|
||||
|
||||
// TranslatePath — edge cases
|
||||
|
||||
// Exact prefix match (path == prefix, no trailing component) is translated.
|
||||
TEST_F(LibcShimTest, ExactPrefixMatchIsTranslated) {
|
||||
RegisterPathMapping("/data/user/0/com.roblox.client",
|
||||
"/host/sober");
|
||||
@@ -126,12 +110,10 @@ TEST_F(LibcShimTest, ExactPrefixMatchIsTranslated) {
|
||||
EXPECT_EQ(TranslatePath("/data/user/0/com.roblox.client"), "/host/sober");
|
||||
}
|
||||
|
||||
// A path that starts with a substring of the registered prefix is NOT
|
||||
// translated (e.g. "/data/user" must not match "/data/user/0/...").
|
||||
// Prefix matching stops at path-component boundaries.
|
||||
TEST_F(LibcShimTest, ShortPathIsNotTranslated) {
|
||||
RegisterPathMapping("/data/user/0/com.roblox.client", "/host/sober");
|
||||
|
||||
// Shorter path: no match expected.
|
||||
EXPECT_EQ(TranslatePath("/data/user"), "/data/user");
|
||||
}
|
||||
|
||||
@@ -142,9 +124,6 @@ TEST_F(LibcShimTest, SimilarPrefixWithoutPathBoundaryIsNotTranslated) {
|
||||
"/data/user/0/com.roblox.client.beta/cache");
|
||||
}
|
||||
|
||||
// RegisterPathMapping — multiple registrations
|
||||
|
||||
// Two different prefixes can coexist and each translates its own paths.
|
||||
TEST_F(LibcShimTest, MultipleRegistrationsCoexist) {
|
||||
RegisterPathMapping("/data/user/0/com.roblox.client", "/host/sober");
|
||||
RegisterPathMapping("/sdcard/Android/data/com.roblox.client",
|
||||
@@ -305,9 +284,6 @@ TEST_F(LibcShimTest, RealpathUsesConfiguredGuestAllocatorForOwnedResult) {
|
||||
std::free(result);
|
||||
}
|
||||
|
||||
// ClearPathMappings
|
||||
|
||||
// After ClearPathMappings, previously registered prefixes no longer match.
|
||||
TEST_F(LibcShimTest, ClearRemovesAllMappings) {
|
||||
RegisterPathMapping("/data/user/0/com.roblox.client", "/host/sober");
|
||||
ClearPathMappings();
|
||||
|
||||
@@ -36,7 +36,7 @@ struct QueueCallbackProbe {
|
||||
int calls = 0;
|
||||
};
|
||||
|
||||
void QueueCallback(abi::AndroidSimpleBufferQueue /*queue*/, void* context) {
|
||||
void QueueCallback(abi::AndroidSimpleBufferQueue, void* context) {
|
||||
auto* probe = static_cast<QueueCallbackProbe*>(context);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(probe->mutex);
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace {
|
||||
jobject g_query_result = nullptr;
|
||||
int g_query_calls = 0;
|
||||
|
||||
jobject QueryFixture(JNIEnv* /*env*/, jclass /*native_gl_class*/) {
|
||||
jobject QueryFixture(JNIEnv*, jclass) {
|
||||
++g_query_calls;
|
||||
return g_query_result;
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ struct ExistingBindingProbe {
|
||||
};
|
||||
|
||||
void ExistingShow(void* context,
|
||||
const jnivm::RobloxTextInputShowRequest& /*request*/) {
|
||||
const jnivm::RobloxTextInputShowRequest&) {
|
||||
++static_cast<ExistingBindingProbe*>(context)->shows;
|
||||
}
|
||||
|
||||
|
||||
@@ -735,7 +735,7 @@ if ! rg -Uq 'SetEnvDefault\(\s*"MOCKTAIL_START_GAME_WITH_PARAM",\s*"0"\s*\)' "${
|
||||
fi
|
||||
|
||||
if rg -q 'MOCKTAIL_UPDATE_SURFACE_GAME_BEFORE_START_GAME|MOCKTAIL_RESUME_GAME_WITH_PLATFORM_PARAMS_AFTER_START_GAME' "${main_cc}"; then
|
||||
echo "typed GAME startup must not retain pre-Start UpdateSurface or post-Start Resume experiment switches" >&2
|
||||
echo "typed GAME startup must not retain pre-Start UpdateSurface or post-Start Resume diagnostic switches" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -858,7 +858,7 @@ fi
|
||||
|
||||
if ! rg -q 'mocktail_stage6_start_lua_return_size_40000' "${main_cc}" ||
|
||||
! rg -q 'MOCKTAIL_STAGE6_START_LUA_TARGET_TABLE_SLOT6_SOURCE' "${main_cc}"; then
|
||||
echo "Stage6 synthetic StartLua target table slot 6 needs an opt-in size-return experiment because resolver treats the slot result as a memory-size request" >&2
|
||||
echo "Stage6 synthetic StartLua target table slot 6 needs an opt-in size-return mode because resolver treats the slot result as a memory-size request" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -898,7 +898,7 @@ if ! rg -q 'MOCKTAIL_PATCH_STAGE6_START_LUA_RESOLVER_RESULT20_CALLBACK' "${main_
|
||||
! rg -Fq 'Stage6 StartLua post-apply task thunk after callback' "${main_cc}" ||
|
||||
! rg -Fq 'Stage6 StartLua resolver result20 split callback context' "${main_cc}" ||
|
||||
! rg -Fq 'Stage6 StartLua resolver result20 callback materialized' "${main_cc}"; then
|
||||
echo "Stage6 resolver experiments must be able to materialize result+0x20 callback pairs under an explicit flag and split callback context/source args at the native call site" >&2
|
||||
echo "Stage6 resolver diagnostics must be able to materialize result+0x20 callback pairs under an explicit flag and split callback context/source args at the native call site" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -943,7 +943,7 @@ fi
|
||||
|
||||
if ! rg -q 'MOCKTAIL_PATCH_STAGE6_START_LUA_POST_APPLY_PAIR_ARGUMENT' "${main_cc}" ||
|
||||
! rg -Fq 'force_pair_argument=%d force_null_argument=%d' "${main_cc}"; then
|
||||
echo "Stage6 post-apply callback experiments must keep pair-argument and null-argument paths explicit in trace logs" >&2
|
||||
echo "Stage6 post-apply callback diagnostics must keep pair-argument and null-argument paths explicit in trace logs" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
Reference in new issue
Block a user