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