mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-17 22:00:46 +02:00
Merge branch 'main' into vr
This commit is contained in:
commit
adccf4e484
20 files changed
+726
-22
No files matched your search
@@ -25,9 +25,19 @@ Release notes on GitHub are taken from the matching section here.
|
||||
edges more precisely and keeps text sharper for a little more GPU time, or `off`.
|
||||
- The log now names each Roblox Java call that Nightcap has no handler for, once per call, as
|
||||
`[JNI] no handler for <class>.<method><signature>`. This shows what a new Roblox version needs.
|
||||
- `--profile` traces now show how much GPU memory decoded ETC2 textures take, as the `emulated texture (MB)`
|
||||
and `compressed texture (MB)` counters. `scripts/summarize_profile_trace.py` prints the peak of each.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Joining a game from a website link while already in a game no longer freezes Nightcap. It leaves the
|
||||
current game and joins the new one, and leaving that game returns to the home screen.
|
||||
|
||||
### Changed
|
||||
|
||||
- The home screen and avatar editor have fewer long frames. PC profiles compile the app's menu scripts to
|
||||
native code and turn this off while you are in a game, so games run as before. Set
|
||||
`FFlagDebugLuauCodegenAll` in `fflags.json` to choose it yourself.
|
||||
- Nightcap answers Roblox's Java calls with less work per call, and the audio thread no longer allocates
|
||||
memory for each block of sound it plays.
|
||||
- VR on Vulkan no longer makes the CPU wait for the GPU each frame. The eye copies, the desktop mirror and
|
||||
|
||||
@@ -281,6 +281,7 @@ add_library(mocktail_runtime STATIC
|
||||
src/runtime/game_session_coordinator.cc
|
||||
src/runtime/graphics_launch_policy.cc
|
||||
src/runtime/http_client_policy.cc
|
||||
src/runtime/luau_codegen_switch.cc
|
||||
src/runtime/memory_limit.cc
|
||||
src/runtime/owned_pthread.cc
|
||||
src/runtime/performance_policy.cc
|
||||
|
||||
@@ -136,6 +136,11 @@ caused by `Color3` errors in the newer SDUI page. An explicit
|
||||
PC profiles also set `FIntFlyoutTestVariantRollout12` to `2`, which shows the
|
||||
desktop side panel menu. An explicit override takes precedence.
|
||||
|
||||
PC profiles compile the app's menus to native code with
|
||||
`FFlagDebugLuauCodegenAll`, and turn it off while you are in a game. Setting
|
||||
it yourself turns the switching off and your value applies everywhere. See
|
||||
[docs/PERFORMANCE.md](docs/PERFORMANCE.md#native-code-in-the-menus).
|
||||
|
||||
```json
|
||||
{
|
||||
"FFlagExample": "True",
|
||||
|
||||
@@ -68,6 +68,12 @@ the CPU time of every thread together, so dividing it by the frame interval
|
||||
gives the number of cores busy. A long frame with little present-thread CPU
|
||||
was spent waiting.
|
||||
|
||||
`emulated texture (MB)` is the VRAM held by the decoded images that stand in
|
||||
for live ETC2/EAC textures, over every mip level and layer, upscaled where
|
||||
small-texture upscale applies. `compressed texture (MB)` is the size the same
|
||||
textures would take as ETC2/EAC. Both change only when such an image is
|
||||
created or destroyed. `summarize_profile_trace.py` prints each one's peak.
|
||||
|
||||
## The benchmark runs
|
||||
|
||||
Run every scenario before and after the change, on the same build type
|
||||
|
||||
+42
-2
@@ -129,14 +129,23 @@ runs cooler for a few frames per second less; measure before keeping it.
|
||||
|
||||
## Small texture upscaling
|
||||
|
||||
`MOCKTAIL_SMALL_TEXTURE_UPSCALE` (default `4`) redraws ETC2 textures of 64px or
|
||||
`MOCKTAIL_SMALL_TEXTURE_UPSCALE` (default `4`) stores ETC2 textures of 64px or
|
||||
smaller at N times their width and height. At the default that is 16 times the
|
||||
pixels per affected texture.
|
||||
|
||||
Upscaling alone adds no detail. Each original pixel becomes an N×N block of the
|
||||
same colour, and the GPU's filtering already blends a small texture that much
|
||||
when it draws it. The extra size only shows when a replacement PNG in
|
||||
`~/.config/mocktail/textures` has more detail than the original.
|
||||
|
||||
The cost lands in two places. Decoding and resampling is CPU work on up to 8
|
||||
threads while textures load, which shows up as a burst rather than a steady
|
||||
load. The enlarged textures then stay in GPU memory for the session and cost
|
||||
extra sampling bandwidth on every frame that uses them.
|
||||
extra sampling bandwidth on every frame that uses them. In Blade Ball the
|
||||
memory share is small: decoded textures came to 5.1 times their ETC2 size both
|
||||
with the default and with upscaling off. The `emulated texture (MB)` and
|
||||
`compressed texture (MB)` counters in a `--profile` trace give the figures for
|
||||
any game.
|
||||
|
||||
That decode does not run on the thread that submits the frame. On a device
|
||||
with a timeline semaphore, uploads decode on a background dispatcher and the
|
||||
@@ -151,6 +160,37 @@ Set it to `1` to turn upscaling off:
|
||||
MOCKTAIL_SMALL_TEXTURE_UPSCALE=1 mocktail
|
||||
```
|
||||
|
||||
## Native code in the menus
|
||||
|
||||
The home screen and avatar editor run Roblox's Lua app in the Luau interpreter,
|
||||
and its long frames come from that thread. PC profiles set
|
||||
`FFlagDebugLuauCodegenAll`, which compiles every script to native code. That
|
||||
makes the menus' Lua about 2.5 times cheaper for about 56 MB of compiled code.
|
||||
|
||||
In a game the same flag compiles every game script too: Blade Ball grew from 7
|
||||
MB to about 300 MB and hit Roblox's native code limit, which shows warnings in
|
||||
F9. So Nightcap turns the flag off when a game starts joining and back on when
|
||||
you return to the menus. Roblox reads the flag each time it compiles a script,
|
||||
and a game's compiled code is freed when you leave it. Games run with Roblox's
|
||||
own per-script choice, the same as without the flag.
|
||||
|
||||
The log shows each switch:
|
||||
|
||||
```text
|
||||
[luau-codegen] menus-only switch at +0x<offset in libroblox.so>
|
||||
[luau-codegen] native code for all scripts off
|
||||
[luau-codegen] native code for all scripts on
|
||||
```
|
||||
|
||||
Setting `FFlagDebugLuauCodegenAll` in `fflags.json` turns the switching off
|
||||
and your value applies everywhere. To keep the interpreter in the menus too:
|
||||
|
||||
```json
|
||||
{
|
||||
"FFlagDebugLuauCodegenAll": false
|
||||
}
|
||||
```
|
||||
|
||||
## Frame rate and present mode
|
||||
|
||||
`graphics.frame_rate_limit` (default `-1`) leaves Roblox's own in-game
|
||||
|
||||
@@ -30,6 +30,18 @@ bool Etc2UploadByteCounts(EtcFormat format, const VkExtent3D& extent,
|
||||
std::uint32_t layer_count, VkDeviceSize* compressed,
|
||||
VkDeviceSize* decoded);
|
||||
|
||||
// Sums every mip level and layer; `scale` multiplies the host image's
|
||||
// level-0 width and height only.
|
||||
bool Etc2ImageByteCounts(EtcFormat format, const VkExtent3D& extent,
|
||||
std::uint32_t mip_levels, std::uint32_t layers,
|
||||
std::uint32_t scale, VkDeviceSize* compressed,
|
||||
VkDeviceSize* decoded);
|
||||
|
||||
struct EmulatedTextureBytes {
|
||||
VkDeviceSize compressed = 0;
|
||||
VkDeviceSize decoded = 0;
|
||||
};
|
||||
|
||||
// Whether emulated ETC2 is reported to the application as a supported
|
||||
// feature, from the MOCKTAIL_ADVERTISE_ETC2 value: "0" hides it, anything
|
||||
// else (or unset) reports it. Hidden ETC2 still decodes ETC2 images the
|
||||
@@ -152,6 +164,10 @@ class VulkanEtc2Emulation final {
|
||||
// pending, after waiting for any decode still writing into them.
|
||||
void ReleaseCommandBuffer(VkCommandBuffer command_buffer);
|
||||
|
||||
// Traced as the "emulated texture (MB)" and "compressed texture (MB)"
|
||||
// counters.
|
||||
EmulatedTextureBytes TextureBytes() const;
|
||||
|
||||
struct State;
|
||||
|
||||
private:
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_LUAU_CODEGEN_SWITCH_H_
|
||||
#define MOCKTAIL_RUNTIME_LUAU_CODEGEN_SWITCH_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "runtime/roblox_experience_presence.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Nightcap defaults this on and keeps it on only outside games; a user value
|
||||
// set in fflags.json or the overrides environment disables the switching.
|
||||
inline constexpr std::string_view kLuauCodegenAllClientSetting =
|
||||
"FFlagDebugLuauCodegenAll";
|
||||
inline constexpr std::string_view kLuauCodegenAllFastFlagName =
|
||||
"DebugLuauCodegenAll";
|
||||
|
||||
struct LoadedImageSegment {
|
||||
std::uintptr_t address = 0;
|
||||
std::size_t size = 0;
|
||||
bool writable = false;
|
||||
};
|
||||
|
||||
std::vector<LoadedImageSegment> LoadedImageSegments(std::uintptr_t image_base);
|
||||
|
||||
// Record layout: {u8 value; pad; const char* name; size_t length}, in a
|
||||
// writable segment filled by libroblox's static constructors. Returns null
|
||||
// unless exactly one record names a NUL-terminated copy of `name`.
|
||||
std::uint8_t* FindFastFlagValue(
|
||||
const std::vector<LoadedImageSegment>& segments, std::string_view name);
|
||||
|
||||
bool ClientSettingsSetLuauCodegenAll(std::string_view overrides_json);
|
||||
|
||||
bool LuauCodegenAllForPhase(RobloxExperiencePresencePhase phase);
|
||||
|
||||
// Locates the flag on the first notification; a byte that is not 0 or 1 is
|
||||
// left alone.
|
||||
class LuauCodegenMenuSwitch {
|
||||
public:
|
||||
using LocateFlagFn = std::function<std::uint8_t*()>;
|
||||
|
||||
LuauCodegenMenuSwitch(LocateFlagFn locate_flag,
|
||||
RobloxExperiencePresenceObserver next);
|
||||
|
||||
RobloxExperiencePresenceObserver observer();
|
||||
void OnPhase(RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest* request);
|
||||
|
||||
private:
|
||||
static void Notify(void* context, RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest* request);
|
||||
|
||||
std::mutex mutex_;
|
||||
LocateFlagFn locate_flag_;
|
||||
bool located_ = false;
|
||||
std::uint8_t* flag_ = nullptr;
|
||||
const RobloxExperiencePresenceObserver next_;
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_LUAU_CODEGEN_SWITCH_H_
|
||||
@@ -6,8 +6,9 @@
|
||||
Usage: summarize_profile_trace.py TRACE [TRACE ...]
|
||||
|
||||
Each trace gets its own column, so `before.json after.json` compares them.
|
||||
Frame times come from the "frame interval (ms)" counter; every other row is
|
||||
a slice name recorded by the Vulkan adapter.
|
||||
Frame times come from the "frame interval (ms)" counter, peaks from the
|
||||
texture counters; every other row is a slice name recorded by the Vulkan
|
||||
adapter.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -20,6 +21,7 @@ import sys
|
||||
from typing import Iterable
|
||||
|
||||
FRAME_COUNTER = "frame interval (ms)"
|
||||
PEAK_COUNTERS = ("emulated texture (MB)", "compressed texture (MB)")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -37,6 +39,7 @@ class Distribution:
|
||||
class Summary:
|
||||
frames: Distribution | None
|
||||
slices: dict[str, Distribution]
|
||||
peaks: dict[str, float]
|
||||
|
||||
|
||||
def _nearest_rank(ordered: list[float], percent: float) -> float:
|
||||
@@ -57,16 +60,21 @@ def _distribution(values: list[float]) -> Distribution:
|
||||
def summarize(events: Iterable[dict]) -> Summary:
|
||||
frames: list[float] = []
|
||||
slices: dict[str, list[float]] = {}
|
||||
peaks: dict[str, float] = {}
|
||||
for event in events:
|
||||
phase = event.get("ph")
|
||||
if phase == "C" and event.get("name") == FRAME_COUNTER:
|
||||
frames.append(float(event["args"]["value"]))
|
||||
elif phase == "C" and event.get("name") in PEAK_COUNTERS:
|
||||
value = float(event["args"]["value"])
|
||||
peaks[event["name"]] = max(peaks.get(event["name"], value), value)
|
||||
elif phase == "X":
|
||||
slices.setdefault(event["name"], []).append(event["dur"] / 1000)
|
||||
return Summary(
|
||||
frames=_distribution(frames) if frames else None,
|
||||
slices={name: _distribution(values)
|
||||
for name, values in slices.items()})
|
||||
for name, values in slices.items()},
|
||||
peaks=peaks)
|
||||
|
||||
|
||||
def _row(label: str, cells: list[str]) -> str:
|
||||
@@ -96,6 +104,11 @@ def to_markdown(traces: list[tuple[str, Summary]]) -> str:
|
||||
if any(frames):
|
||||
lines += [_metric_row(f"frame interval {field} (ms)", frames, field)
|
||||
for field in ("mean", "p50", "p95", "p99", "max")]
|
||||
for name in PEAK_COUNTERS:
|
||||
if any(name in summary.peaks for summary in summaries):
|
||||
lines.append(_row(f"{name} peak", [
|
||||
f"{summary.peaks[name]:.2f}" if name in summary.peaks else "-"
|
||||
for summary in summaries]))
|
||||
|
||||
# Largest total time first, taken from whichever trace spent the most.
|
||||
totals: dict[str, float] = {}
|
||||
|
||||
@@ -92,6 +92,34 @@ bool Etc2UploadByteCounts(EtcFormat format, const VkExtent3D& extent,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Etc2ImageByteCounts(EtcFormat format, const VkExtent3D& extent,
|
||||
std::uint32_t mip_levels, std::uint32_t layers,
|
||||
std::uint32_t scale, VkDeviceSize* compressed,
|
||||
VkDeviceSize* decoded) {
|
||||
if (extent.width == 0 || extent.height == 0 || extent.depth == 0 ||
|
||||
mip_levels == 0 || layers == 0 || scale == 0 || compressed == nullptr ||
|
||||
decoded == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const auto level = [](std::uint32_t size, std::uint32_t mip) {
|
||||
return std::max<VkDeviceSize>(1, static_cast<VkDeviceSize>(size) >> mip);
|
||||
};
|
||||
VkDeviceSize compressed_total = 0;
|
||||
VkDeviceSize decoded_total = 0;
|
||||
for (std::uint32_t mip = 0; mip < mip_levels && mip < 32; ++mip) {
|
||||
const VkDeviceSize depth = level(extent.depth, mip);
|
||||
compressed_total += ((level(extent.width, mip) + 3) / 4) *
|
||||
((level(extent.height, mip) + 3) / 4) * depth *
|
||||
EtcBlockBytes(format);
|
||||
decoded_total += level(extent.width * scale, mip) *
|
||||
level(extent.height * scale, mip) * depth *
|
||||
EtcDecodedTexelBytes(format);
|
||||
}
|
||||
*compressed = compressed_total * layers;
|
||||
*decoded = decoded_total * layers;
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool EmulationDisabledByEnvironment() {
|
||||
@@ -207,8 +235,21 @@ struct ImageRecord {
|
||||
// Decoded level-0 texels of a scaled image; every host mip is resampled
|
||||
// from them so the texture keeps its detail at every draw size.
|
||||
std::shared_ptr<const RgbaImage> base;
|
||||
EmulatedTextureBytes bytes;
|
||||
};
|
||||
|
||||
// `now` is read under the lock that produced `totals`, so concurrent changes
|
||||
// reach the trace in the order they happened.
|
||||
void TraceTextureBytes(const EmulatedTextureBytes& totals, std::uint64_t now) {
|
||||
ChromeTraceWriter* const trace = ActiveProfileTrace();
|
||||
if (trace == nullptr) {
|
||||
return;
|
||||
}
|
||||
constexpr double kMegabyte = 1024.0 * 1024.0;
|
||||
trace->Counter("emulated texture (MB)", now, totals.decoded / kMegabyte);
|
||||
trace->Counter("compressed texture (MB)", now, totals.compressed / kMegabyte);
|
||||
}
|
||||
|
||||
bool IsColorFormat(EtcFormat format) {
|
||||
return format == EtcFormat::kEtc2Rgb8 || format == EtcFormat::kEtc2Rgb8A1 ||
|
||||
format == EtcFormat::kEtc2Rgba8;
|
||||
@@ -669,6 +710,8 @@ struct VulkanEtc2Emulation::State {
|
||||
std::unordered_map<VkPhysicalDevice, bool> physical_devices;
|
||||
std::vector<std::unique_ptr<HostDevice>> devices;
|
||||
std::unordered_map<VkImage, ImageRecord> images;
|
||||
// Sum over `images`; guarded by `mutex`.
|
||||
EmulatedTextureBytes texture_bytes;
|
||||
std::unordered_map<VkBuffer, BufferBinding> buffers;
|
||||
std::unordered_map<VkDeviceMemory, Mapping> mappings;
|
||||
std::unordered_map<VkCommandBuffer, CommandRecord> commands;
|
||||
@@ -1245,6 +1288,8 @@ void VulkanEtc2Emulation::DestroyDevice(VkDevice device) {
|
||||
// Nothing may free staging or the semaphore while a decode still writes it.
|
||||
state_->DrainDecodes();
|
||||
std::vector<std::unique_ptr<StagingBlock>> doomed;
|
||||
EmulatedTextureBytes totals;
|
||||
std::uint64_t now = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
for (auto it = state_->commands.begin(); it != state_->commands.end();) {
|
||||
@@ -1265,11 +1310,20 @@ void VulkanEtc2Emulation::DestroyDevice(VkDevice device) {
|
||||
}
|
||||
}
|
||||
for (auto it = state_->images.begin(); it != state_->images.end();) {
|
||||
it = it->second.device == device ? state_->images.erase(it) : std::next(it);
|
||||
if (it->second.device == device) {
|
||||
state_->texture_bytes.compressed -= it->second.bytes.compressed;
|
||||
state_->texture_bytes.decoded -= it->second.bytes.decoded;
|
||||
it = state_->images.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
totals = state_->texture_bytes;
|
||||
now = TraceClockNanos();
|
||||
state_->has_commands.store(!state_->commands.empty(),
|
||||
std::memory_order_release);
|
||||
}
|
||||
TraceTextureBytes(totals, now);
|
||||
if (const HostDevice* dev = state_->Find(device); dev != nullptr) {
|
||||
for (const auto& block : doomed) {
|
||||
DestroyStagingBlock(*dev, *block);
|
||||
@@ -1316,6 +1370,14 @@ VkResult VulkanEtc2Emulation::CreateImage(VkDevice device,
|
||||
}
|
||||
const VkResult result = dev->create_image(device, &host_info, allocator, image);
|
||||
if (result == VK_SUCCESS && image != nullptr) {
|
||||
EmulatedTextureBytes bytes;
|
||||
if (!Etc2ImageByteCounts(format.etc_format, create_info->extent,
|
||||
create_info->mipLevels, create_info->arrayLayers,
|
||||
scale, &bytes.compressed, &bytes.decoded)) {
|
||||
bytes = {};
|
||||
}
|
||||
EmulatedTextureBytes totals;
|
||||
std::uint64_t now = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
state_->images[*image] = {device,
|
||||
@@ -1324,8 +1386,14 @@ VkResult VulkanEtc2Emulation::CreateImage(VkDevice device,
|
||||
create_info->extent.width,
|
||||
create_info->extent.height,
|
||||
nullptr,
|
||||
nullptr};
|
||||
nullptr,
|
||||
bytes};
|
||||
state_->texture_bytes.compressed += bytes.compressed;
|
||||
state_->texture_bytes.decoded += bytes.decoded;
|
||||
totals = state_->texture_bytes;
|
||||
now = TraceClockNanos();
|
||||
}
|
||||
TraceTextureBytes(totals, now);
|
||||
if (ShouldLog(&g_image_logs, 4)) {
|
||||
std::fprintf(stderr,
|
||||
" [vulkan] ETC2 image %ux%u mips=%u format=%d -> host "
|
||||
@@ -1346,12 +1414,33 @@ void VulkanEtc2Emulation::DestroyImage(VkDevice device, VkImage image,
|
||||
return;
|
||||
}
|
||||
if (dev->emulated) {
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
state_->images.erase(image);
|
||||
EmulatedTextureBytes totals;
|
||||
std::uint64_t now = 0;
|
||||
bool changed = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
const auto it = state_->images.find(image);
|
||||
if (it != state_->images.end()) {
|
||||
state_->texture_bytes.compressed -= it->second.bytes.compressed;
|
||||
state_->texture_bytes.decoded -= it->second.bytes.decoded;
|
||||
state_->images.erase(it);
|
||||
totals = state_->texture_bytes;
|
||||
now = TraceClockNanos();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
TraceTextureBytes(totals, now);
|
||||
}
|
||||
}
|
||||
dev->destroy_image(device, image, allocator);
|
||||
}
|
||||
|
||||
EmulatedTextureBytes VulkanEtc2Emulation::TextureBytes() const {
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
return state_->texture_bytes;
|
||||
}
|
||||
|
||||
VkResult VulkanEtc2Emulation::CreateImageView(
|
||||
VkDevice device, const VkImageViewCreateInfo* create_info,
|
||||
const VkAllocationCallbacks* allocator, VkImageView* view) {
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
#include "runtime/display_size.h"
|
||||
#include "runtime/environment.h"
|
||||
#include "runtime/discord_rpc.h"
|
||||
#include "runtime/luau_codegen_switch.h"
|
||||
#include "runtime/jnivm_platform_web_callbacks.h"
|
||||
#include "runtime/owned_pthread.h"
|
||||
#include "runtime/platform_cache_migration.h"
|
||||
@@ -4692,6 +4693,27 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
<< "; continuing without Rich Presence\n";
|
||||
}
|
||||
}
|
||||
mocktail::runtime::LuauCodegenMenuSwitch luau_codegen_switch(
|
||||
[&process_environment]() -> std::uint8_t* {
|
||||
if (process_environment.GetOr("MOCKTAIL_LUAU_CODEGEN_MENU_SWITCH",
|
||||
"0") != "1") {
|
||||
return nullptr;
|
||||
}
|
||||
const std::uintptr_t base = g_libroblox_base;
|
||||
std::uint8_t* flag = mocktail::runtime::FindFastFlagValue(
|
||||
mocktail::runtime::LoadedImageSegments(base),
|
||||
mocktail::runtime::kLuauCodegenAllFastFlagName);
|
||||
if (flag == nullptr) {
|
||||
std::cerr << " [luau-codegen] DebugLuauCodegenAll not found; "
|
||||
"leaving it on in games\n";
|
||||
} else {
|
||||
std::cout << " [luau-codegen] menus-only switch at +0x" << std::hex
|
||||
<< (reinterpret_cast<std::uintptr_t>(flag) - base)
|
||||
<< std::dec << '\n';
|
||||
}
|
||||
return flag;
|
||||
},
|
||||
discord_rpc.observer());
|
||||
const mocktail::runtime::InputCapabilityConfig& input_capabilities =
|
||||
runtime_config.input_capabilities();
|
||||
if (!runtime_config.frame_rate().valid()) {
|
||||
@@ -6917,7 +6939,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
jni_factory, present_boundary, std::move(surface_config),
|
||||
&dependencies.roblox_credential(),
|
||||
mocktail::runtime::RobloxExperienceSurfaceProvider{},
|
||||
discord_rpc.observer(),
|
||||
luau_codegen_switch.observer(),
|
||||
dependencies.clear_persisted_web_view_cookie(),
|
||||
runtime_config.microphone_enabled(),
|
||||
platform_web_symbols.account);
|
||||
|
||||
+10
-3
@@ -38,6 +38,7 @@
|
||||
#include "runtime/fleasion.h"
|
||||
#include "runtime/game_mode.h"
|
||||
#include "runtime/graphics_launch_policy.h"
|
||||
#include "runtime/luau_codegen_switch.h"
|
||||
#include "runtime/memory_limit.h"
|
||||
#include "runtime/payload_update_preflight.h"
|
||||
#include "runtime/performance_policy.h"
|
||||
@@ -1218,6 +1219,7 @@ int main(int argc, char* argv[]) {
|
||||
std::cout
|
||||
<< " [auth] explicit guest identity selected for production VM\n";
|
||||
}
|
||||
unsetenv("MOCKTAIL_LUAU_CODEGEN_MENU_SWITCH");
|
||||
if (runtime_config.config.desktop_playability()) {
|
||||
std::filesystem::path assets = paths.DefaultAssetPath();
|
||||
if (environment.HasNonEmpty("MOCKTAIL_ASSET_PATH")) {
|
||||
@@ -1240,10 +1242,13 @@ int main(int argc, char* argv[]) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
std::string desktop_client_settings;
|
||||
const std::string user_client_settings =
|
||||
environment.GetOr("MOCKTAIL_CLIENT_SETTINGS_OVERRIDES_JSON", "{}");
|
||||
const bool user_set_luau_codegen =
|
||||
mocktail::runtime::ClientSettingsSetLuauCodegenAll(
|
||||
user_client_settings);
|
||||
if (!mocktail::runtime::MergeDesktopAppPolicyClientSettingsOverride(
|
||||
desktop_policy.policy_json,
|
||||
environment.GetOr("MOCKTAIL_CLIENT_SETTINGS_OVERRIDES_JSON",
|
||||
"{}"),
|
||||
desktop_policy.policy_json, user_client_settings,
|
||||
&desktop_client_settings, &command_line_error)) {
|
||||
std::cerr << "[FATAL] Cannot activate desktop Roblox app-policy: "
|
||||
<< command_line_error << '\n';
|
||||
@@ -1254,6 +1259,8 @@ int main(int argc, char* argv[]) {
|
||||
std::cerr << "[FATAL] Cannot export desktop Roblox app-policy\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
setenv("MOCKTAIL_LUAU_CODEGEN_MENU_SWITCH",
|
||||
user_set_luau_codegen ? "0" : "1", 1);
|
||||
std::cout << " [runtime] desktop app-policy ready: normalized="
|
||||
<< desktop_policy.normalized_policy_count
|
||||
<< " updated=" << (desktop_policy.updated ? 1 : 0)
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
#include "runtime/luau_codegen_switch.h"
|
||||
|
||||
#include <elf.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kNameSlotOffset = 8;
|
||||
constexpr std::size_t kRecordTailSize = 16;
|
||||
|
||||
std::string_view SegmentBytes(const LoadedImageSegment& segment) {
|
||||
return {reinterpret_cast<const char*>(segment.address), segment.size};
|
||||
}
|
||||
|
||||
std::vector<std::uintptr_t> NameAddresses(
|
||||
const std::vector<LoadedImageSegment>& segments, std::string_view name) {
|
||||
std::string needle(name);
|
||||
needle.push_back('\0');
|
||||
std::vector<std::uintptr_t> addresses;
|
||||
for (const LoadedImageSegment& segment : segments) {
|
||||
const std::string_view bytes = SegmentBytes(segment);
|
||||
for (std::size_t at = bytes.find(needle); at != std::string_view::npos;
|
||||
at = bytes.find(needle, at + 1)) {
|
||||
addresses.push_back(segment.address + at);
|
||||
}
|
||||
}
|
||||
return addresses;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<LoadedImageSegment> LoadedImageSegments(std::uintptr_t image_base) {
|
||||
std::vector<LoadedImageSegment> segments;
|
||||
if (image_base == 0) {
|
||||
return segments;
|
||||
}
|
||||
const auto* header = reinterpret_cast<const Elf64_Ehdr*>(image_base);
|
||||
if (std::memcmp(header->e_ident, ELFMAG, SELFMAG) != 0 ||
|
||||
header->e_ident[EI_CLASS] != ELFCLASS64 || header->e_phoff == 0) {
|
||||
return segments;
|
||||
}
|
||||
const auto* program = reinterpret_cast<const Elf64_Phdr*>(
|
||||
image_base + static_cast<std::uintptr_t>(header->e_phoff));
|
||||
for (std::size_t index = 0; index < header->e_phnum; ++index) {
|
||||
if (program[index].p_type != PT_LOAD || program[index].p_memsz == 0) {
|
||||
continue;
|
||||
}
|
||||
segments.push_back({image_base + program[index].p_vaddr,
|
||||
static_cast<std::size_t>(program[index].p_memsz),
|
||||
(program[index].p_flags & PF_W) != 0});
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
std::uint8_t* FindFastFlagValue(const std::vector<LoadedImageSegment>& segments,
|
||||
std::string_view name) {
|
||||
const std::vector<std::uintptr_t> names = NameAddresses(segments, name);
|
||||
if (names.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
std::uint8_t* found = nullptr;
|
||||
int matches = 0;
|
||||
for (const LoadedImageSegment& segment : segments) {
|
||||
if (!segment.writable || segment.size < kNameSlotOffset + kRecordTailSize) {
|
||||
continue;
|
||||
}
|
||||
const std::uintptr_t first =
|
||||
(segment.address + kNameSlotOffset + 7) & ~std::uintptr_t{7};
|
||||
const std::uintptr_t end = segment.address + segment.size;
|
||||
for (std::uintptr_t slot = first; slot + kRecordTailSize <= end;
|
||||
slot += 8) {
|
||||
std::uintptr_t pointer = 0;
|
||||
std::memcpy(&pointer, reinterpret_cast<const void*>(slot),
|
||||
sizeof(pointer));
|
||||
if (std::find(names.begin(), names.end(), pointer) == names.end()) {
|
||||
continue;
|
||||
}
|
||||
std::uint64_t length = 0;
|
||||
std::memcpy(&length, reinterpret_cast<const void*>(slot + 8),
|
||||
sizeof(length));
|
||||
if (length != name.size()) {
|
||||
continue;
|
||||
}
|
||||
found = reinterpret_cast<std::uint8_t*>(slot - kNameSlotOffset);
|
||||
++matches;
|
||||
}
|
||||
}
|
||||
return matches == 1 ? found : nullptr;
|
||||
}
|
||||
|
||||
bool ClientSettingsSetLuauCodegenAll(std::string_view overrides_json) {
|
||||
const nlohmann::json overrides =
|
||||
nlohmann::json::parse(overrides_json, nullptr, false, true);
|
||||
return overrides.is_object() &&
|
||||
overrides.contains(std::string(kLuauCodegenAllClientSetting));
|
||||
}
|
||||
|
||||
bool LuauCodegenAllForPhase(RobloxExperiencePresencePhase phase) {
|
||||
return phase == RobloxExperiencePresencePhase::kBrowsing;
|
||||
}
|
||||
|
||||
LuauCodegenMenuSwitch::LuauCodegenMenuSwitch(
|
||||
LocateFlagFn locate_flag, RobloxExperiencePresenceObserver next)
|
||||
: locate_flag_(std::move(locate_flag)), next_(next) {}
|
||||
|
||||
RobloxExperiencePresenceObserver LuauCodegenMenuSwitch::observer() {
|
||||
return {this, &LuauCodegenMenuSwitch::Notify};
|
||||
}
|
||||
|
||||
void LuauCodegenMenuSwitch::OnPhase(
|
||||
RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest* request) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!located_) {
|
||||
located_ = true;
|
||||
flag_ = locate_flag_ ? locate_flag_() : nullptr;
|
||||
}
|
||||
if (flag_ != nullptr) {
|
||||
const std::uint8_t current = __atomic_load_n(flag_, __ATOMIC_RELAXED);
|
||||
const std::uint8_t wanted = LuauCodegenAllForPhase(phase) ? 1 : 0;
|
||||
if (current <= 1 && current != wanted) {
|
||||
__atomic_store_n(flag_, wanted, __ATOMIC_RELAXED);
|
||||
std::cout << " [luau-codegen] native code for all scripts "
|
||||
<< (wanted != 0 ? "on" : "off") << '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (next_.valid()) {
|
||||
next_.notify(next_.context, phase, request);
|
||||
}
|
||||
}
|
||||
|
||||
void LuauCodegenMenuSwitch::Notify(
|
||||
void* context, RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest* request) {
|
||||
static_cast<LuauCodegenMenuSwitch*>(context)->OnPhase(phase, request);
|
||||
}
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
#include "runtime/luau_codegen_switch.h"
|
||||
#include "runtime/runtime_paths.h"
|
||||
|
||||
namespace mocktail {
|
||||
@@ -359,6 +360,9 @@ bool MergeDesktopAppPolicyClientSettingsOverride(std::string_view policy_json,
|
||||
return false;
|
||||
}
|
||||
overrides[std::string(kDesktopAppPolicyOverride)] = policy_json;
|
||||
// Native Luau code makes the app's menus faster. LuauCodegenMenuSwitch turns
|
||||
// it off in games, which keep Roblox's own per-script choice.
|
||||
overrides.emplace(std::string(kLuauCodegenAllClientSetting), "True");
|
||||
// The SDUI Charts page in 2.736/2.738 can pass a table to TextColor3 and
|
||||
// leave the whole LuaApp blank. Default to the working legacy page while
|
||||
// preserving explicit user overrides for testing newer UI patches.
|
||||
|
||||
@@ -1461,8 +1461,9 @@ Status RobloxExperienceComposition::DrainLaunchRequests() {
|
||||
return Status::Ok();
|
||||
if (!launch_in_progress_ && active_launch_ == nullptr && game_active_ &&
|
||||
!pending_launch_requests_.empty()) {
|
||||
// Start the queued launch only after consuming this leave callback, so
|
||||
// it cannot reach the next controller.
|
||||
// Blocks a reentrant drain while the leave pumps main-thread messages.
|
||||
// The leave ends in the Native stage with no Lua app return, so the
|
||||
// queued launch starts on the next drain.
|
||||
controlled_switch_waiting_for_return_ = true;
|
||||
switch_active_game = true;
|
||||
}
|
||||
@@ -1471,7 +1472,7 @@ Status RobloxExperienceComposition::DrainLaunchRequests() {
|
||||
std::fprintf(stderr,
|
||||
"[experience] leaving active game for queued launch\n");
|
||||
const Status leave_status = LeaveGame();
|
||||
if (!leave_status.ok()) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
controlled_switch_waiting_for_return_ = false;
|
||||
}
|
||||
|
||||
@@ -402,6 +402,13 @@ target_link_libraries(roblox_desktop_app_policy_test PRIVATE
|
||||
)
|
||||
mocktail_apply_compile_options(roblox_desktop_app_policy_test)
|
||||
|
||||
add_executable(luau_codegen_switch_test luau_codegen_switch_test.cc)
|
||||
target_link_libraries(luau_codegen_switch_test PRIVATE
|
||||
Mocktail::Runtime
|
||||
GTest::gtest_main
|
||||
)
|
||||
mocktail_apply_compile_options(luau_codegen_switch_test)
|
||||
|
||||
add_executable(browser_tracker_service_test browser_tracker_service_test.cc)
|
||||
target_link_libraries(browser_tracker_service_test PRIVATE
|
||||
Mocktail::Services
|
||||
@@ -1290,6 +1297,7 @@ if(MOCKTAIL_ENABLE_UPSTREAM_JNIVM)
|
||||
endif()
|
||||
gtest_discover_tests(libc_shim_test)
|
||||
gtest_discover_tests(discord_rpc_test)
|
||||
gtest_discover_tests(luau_codegen_switch_test)
|
||||
gtest_discover_tests(asset_manager_test)
|
||||
gtest_discover_tests(android_looper_test)
|
||||
gtest_discover_tests(android_native_window_test)
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
#include "runtime/luau_codegen_switch.h"
|
||||
|
||||
#include <elf.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::runtime {
|
||||
namespace {
|
||||
|
||||
constexpr char kStrings[] = "\0FFlagOther\0DebugLuauCodegenAll\0tail";
|
||||
|
||||
struct alignas(8) FlagRecord {
|
||||
std::uint8_t value = 1;
|
||||
std::uint8_t padding[7] = {};
|
||||
const char* name = nullptr;
|
||||
std::uint64_t name_length = 0;
|
||||
std::uint64_t next = 0;
|
||||
};
|
||||
|
||||
const char* NameInStrings() {
|
||||
return kStrings + 12;
|
||||
}
|
||||
|
||||
std::vector<LoadedImageSegment> Segments(std::vector<FlagRecord>& records) {
|
||||
return {
|
||||
{reinterpret_cast<std::uintptr_t>(kStrings), sizeof(kStrings), false},
|
||||
{reinterpret_cast<std::uintptr_t>(records.data()),
|
||||
records.size() * sizeof(FlagRecord), true},
|
||||
};
|
||||
}
|
||||
|
||||
TEST(LuauCodegenSwitchTest, FindsTheFlagRecordByName) {
|
||||
ASSERT_EQ(std::string(NameInStrings()), "DebugLuauCodegenAll");
|
||||
std::vector<FlagRecord> records(3);
|
||||
records[1].name = NameInStrings();
|
||||
records[1].name_length = 19;
|
||||
const auto segments = Segments(records);
|
||||
EXPECT_EQ(FindFastFlagValue(segments, kLuauCodegenAllFastFlagName),
|
||||
&records[1].value);
|
||||
}
|
||||
|
||||
TEST(LuauCodegenSwitchTest, RejectsWrongLengthMissingOrDuplicateRecords) {
|
||||
std::vector<FlagRecord> records(3);
|
||||
records[1].name = NameInStrings();
|
||||
records[1].name_length = 18;
|
||||
auto segments = Segments(records);
|
||||
EXPECT_EQ(FindFastFlagValue(segments, kLuauCodegenAllFastFlagName), nullptr);
|
||||
|
||||
records[1].name_length = 19;
|
||||
EXPECT_EQ(FindFastFlagValue(segments, "DebugLuauCodegen"), nullptr);
|
||||
|
||||
records[2].name = NameInStrings();
|
||||
records[2].name_length = 19;
|
||||
EXPECT_EQ(FindFastFlagValue(segments, kLuauCodegenAllFastFlagName), nullptr);
|
||||
}
|
||||
|
||||
TEST(LuauCodegenSwitchTest, IgnoresRecordsInReadOnlySegments) {
|
||||
std::vector<FlagRecord> records(1);
|
||||
records[0].name = NameInStrings();
|
||||
records[0].name_length = 19;
|
||||
auto segments = Segments(records);
|
||||
segments[1].writable = false;
|
||||
EXPECT_EQ(FindFastFlagValue(segments, kLuauCodegenAllFastFlagName), nullptr);
|
||||
}
|
||||
|
||||
TEST(LuauCodegenSwitchTest, ReadsLoadSegmentsFromTheElfHeader) {
|
||||
alignas(8) std::uint8_t image[0x400] = {};
|
||||
auto* header = reinterpret_cast<Elf64_Ehdr*>(image);
|
||||
std::memcpy(header->e_ident, ELFMAG, SELFMAG);
|
||||
header->e_ident[EI_CLASS] = ELFCLASS64;
|
||||
header->e_phoff = sizeof(Elf64_Ehdr);
|
||||
header->e_phnum = 3;
|
||||
auto* program = reinterpret_cast<Elf64_Phdr*>(image + header->e_phoff);
|
||||
program[0] = {PT_LOAD, PF_R | PF_X, 0, 0, 0, 0x200, 0x200, 0x1000};
|
||||
program[1] = {PT_DYNAMIC, PF_R | PF_W, 0, 0x200, 0, 0x10, 0x10, 8};
|
||||
program[2] = {PT_LOAD, PF_R | PF_W, 0, 0x200, 0, 0x80, 0x180, 0x1000};
|
||||
const auto base = reinterpret_cast<std::uintptr_t>(image);
|
||||
const auto segments = LoadedImageSegments(base);
|
||||
ASSERT_EQ(segments.size(), 2u);
|
||||
EXPECT_EQ(segments[0].address, base);
|
||||
EXPECT_EQ(segments[0].size, 0x200u);
|
||||
EXPECT_FALSE(segments[0].writable);
|
||||
EXPECT_EQ(segments[1].address, base + 0x200);
|
||||
EXPECT_EQ(segments[1].size, 0x180u);
|
||||
EXPECT_TRUE(segments[1].writable);
|
||||
|
||||
std::memset(image, 0, SELFMAG);
|
||||
EXPECT_TRUE(LoadedImageSegments(base).empty());
|
||||
}
|
||||
|
||||
TEST(LuauCodegenSwitchTest, DetectsAUserSetting) {
|
||||
EXPECT_FALSE(ClientSettingsSetLuauCodegenAll("{}"));
|
||||
EXPECT_FALSE(ClientSettingsSetLuauCodegenAll(R"({"FIntOther":1})"));
|
||||
EXPECT_FALSE(ClientSettingsSetLuauCodegenAll("not-json"));
|
||||
EXPECT_TRUE(
|
||||
ClientSettingsSetLuauCodegenAll(R"({"FFlagDebugLuauCodegenAll":true})"));
|
||||
EXPECT_TRUE(
|
||||
ClientSettingsSetLuauCodegenAll(R"({"FFlagDebugLuauCodegenAll":"False"})"));
|
||||
}
|
||||
|
||||
TEST(LuauCodegenSwitchTest, EnablesCodegenOnlyWhileBrowsing) {
|
||||
EXPECT_TRUE(LuauCodegenAllForPhase(RobloxExperiencePresencePhase::kBrowsing));
|
||||
EXPECT_FALSE(LuauCodegenAllForPhase(RobloxExperiencePresencePhase::kJoining));
|
||||
EXPECT_FALSE(LuauCodegenAllForPhase(RobloxExperiencePresencePhase::kPlaying));
|
||||
}
|
||||
|
||||
struct Forwarded {
|
||||
int count = 0;
|
||||
RobloxExperiencePresencePhase phase = RobloxExperiencePresencePhase::kBrowsing;
|
||||
};
|
||||
|
||||
void RecordForward(void* context, RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest*) {
|
||||
auto* forwarded = static_cast<Forwarded*>(context);
|
||||
++forwarded->count;
|
||||
forwarded->phase = phase;
|
||||
}
|
||||
|
||||
TEST(LuauCodegenSwitchTest, WritesTheFlagPerPhaseAndForwards) {
|
||||
std::uint8_t flag = 1;
|
||||
int locate_calls = 0;
|
||||
Forwarded forwarded;
|
||||
LuauCodegenMenuSwitch menu_switch(
|
||||
[&] {
|
||||
++locate_calls;
|
||||
return &flag;
|
||||
},
|
||||
{&forwarded, &RecordForward});
|
||||
const RobloxExperiencePresenceObserver observer = menu_switch.observer();
|
||||
ASSERT_TRUE(observer.valid());
|
||||
|
||||
observer.notify(observer.context, RobloxExperiencePresencePhase::kJoining,
|
||||
nullptr);
|
||||
EXPECT_EQ(flag, 0);
|
||||
observer.notify(observer.context, RobloxExperiencePresencePhase::kPlaying,
|
||||
nullptr);
|
||||
EXPECT_EQ(flag, 0);
|
||||
observer.notify(observer.context, RobloxExperiencePresencePhase::kBrowsing,
|
||||
nullptr);
|
||||
EXPECT_EQ(flag, 1);
|
||||
EXPECT_EQ(locate_calls, 1);
|
||||
EXPECT_EQ(forwarded.count, 3);
|
||||
EXPECT_EQ(forwarded.phase, RobloxExperiencePresencePhase::kBrowsing);
|
||||
}
|
||||
|
||||
TEST(LuauCodegenSwitchTest, LeavesUnexpectedValuesAndMissingFlagsAlone) {
|
||||
std::uint8_t flag = 7;
|
||||
LuauCodegenMenuSwitch menu_switch([&] { return &flag; }, {});
|
||||
menu_switch.OnPhase(RobloxExperiencePresencePhase::kJoining, nullptr);
|
||||
EXPECT_EQ(flag, 7);
|
||||
|
||||
LuauCodegenMenuSwitch missing([] { return nullptr; }, {});
|
||||
missing.OnPhase(RobloxExperiencePresencePhase::kJoining, nullptr);
|
||||
EXPECT_TRUE(missing.observer().valid());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mocktail::runtime
|
||||
@@ -236,6 +236,22 @@ TEST(RobloxDesktopAppPolicyTest, DefaultsDesktopFlyoutSidePanelUnlessOverridden)
|
||||
"0");
|
||||
}
|
||||
|
||||
TEST(RobloxDesktopAppPolicyTest, DefaultsLuauCodegenAllUnlessOverridden) {
|
||||
const std::string policy =
|
||||
nlohmann::json({{"PlatformGroup", "Desktop"}}).dump();
|
||||
std::string merged;
|
||||
std::string error;
|
||||
ASSERT_TRUE(MergeDesktopAppPolicyClientSettingsOverride(policy, "{}", &merged,
|
||||
&error))
|
||||
<< error;
|
||||
EXPECT_EQ(nlohmann::json::parse(merged)["FFlagDebugLuauCodegenAll"], "True");
|
||||
|
||||
ASSERT_TRUE(MergeDesktopAppPolicyClientSettingsOverride(
|
||||
policy, R"({"FFlagDebugLuauCodegenAll":false})", &merged, &error))
|
||||
<< error;
|
||||
EXPECT_EQ(nlohmann::json::parse(merged)["FFlagDebugLuauCodegenAll"], false);
|
||||
}
|
||||
|
||||
TEST(RobloxDesktopAppPolicyTest, RejectsMalformedOrSymlinkedStorage) {
|
||||
TemporaryDirectory temporary;
|
||||
const std::filesystem::path default_path = temporary.path() / "default.json";
|
||||
|
||||
@@ -816,13 +816,9 @@ TEST(RobloxExperienceCompositionTest,
|
||||
ASSERT_TRUE(composition.DrainLaunchRequests().ok());
|
||||
EXPECT_EQ(probe.leaves, 1);
|
||||
EXPECT_EQ(probe.starts.load(), 1);
|
||||
ASSERT_TRUE(composition.DrainLaunchRequests().ok());
|
||||
EXPECT_EQ(probe.leaves, 1);
|
||||
EXPECT_EQ(probe.starts.load(), 1);
|
||||
|
||||
// The exact gameDidLeave callback is the safe boundary between the old
|
||||
// controller and the replacement server launch.
|
||||
composition.NotifyLuaAppDidReturn();
|
||||
// nativeAppBridgeV2LeaveGame ends in the Native stage and Roblox sends no
|
||||
// Lua app return, so the queued launch starts once the leave has returned.
|
||||
for (int attempt = 0; attempt < 1000 && probe.starts.load() < 2; ++attempt) {
|
||||
ASSERT_TRUE(composition.DrainLaunchRequests().ok());
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
|
||||
@@ -89,6 +89,20 @@ class SummarizeTest(unittest.TestCase):
|
||||
self.assertIn("| frame interval p50 (ms) | 20.00 | 10.00 |", lines)
|
||||
self.assertIn("| a mean (ms) | 1.00 | 0.50 |", lines)
|
||||
|
||||
def test_texture_counters_report_their_peak(self) -> None:
|
||||
events = [
|
||||
{"name": "emulated texture (MB)", "ph": "C", "ts": ts, "pid": 1,
|
||||
"tid": 1, "args": {"value": value}}
|
||||
for ts, value in ((1, 10.0), (2, 300.0), (3, 120.0))
|
||||
]
|
||||
with_textures = summary.summarize(events)
|
||||
without = summary.summarize([])
|
||||
|
||||
self.assertEqual(with_textures.peaks["emulated texture (MB)"], 300.0)
|
||||
table = summary.to_markdown([("one", with_textures), ("two", without)])
|
||||
self.assertIn("| emulated texture (MB) peak | 300.00 | - |",
|
||||
table.splitlines())
|
||||
|
||||
def test_markdown_marks_missing_values(self) -> None:
|
||||
with_slice = summary.summarize([slice_event("a", 1_000)])
|
||||
without = summary.summarize([])
|
||||
|
||||
@@ -102,6 +102,37 @@ TEST(VulkanEtc2EmulationTest, CountsUploadBytesPerRegion) {
|
||||
EXPECT_EQ(decoded, 2U);
|
||||
}
|
||||
|
||||
TEST(VulkanEtc2EmulationTest, CountsWholeImageBytesAcrossMipsAndLayers) {
|
||||
VkDeviceSize compressed = 0;
|
||||
VkDeviceSize decoded = 0;
|
||||
// 64x64 with a full chain: 64, 32, 16, 8, 4, 2, 1.
|
||||
ASSERT_TRUE(Etc2ImageByteCounts(EtcFormat::kEtc2Rgb8, {64, 64, 1}, 7, 1, 1,
|
||||
&compressed, &decoded));
|
||||
EXPECT_EQ(compressed, 343U * 8U);
|
||||
EXPECT_EQ(decoded, 5461U * 4U);
|
||||
|
||||
// Two layers of EAC R11, 2 bytes per decoded texel.
|
||||
ASSERT_TRUE(Etc2ImageByteCounts(EtcFormat::kEacR11, {8, 4, 1}, 1, 2, 1,
|
||||
&compressed, &decoded));
|
||||
EXPECT_EQ(compressed, 2U * 2U * 8U);
|
||||
EXPECT_EQ(decoded, 2U * 32U * 2U);
|
||||
}
|
||||
|
||||
TEST(VulkanEtc2EmulationTest, CountsDecodedBytesAtTheUpscaledExtent) {
|
||||
VkDeviceSize compressed = 0;
|
||||
VkDeviceSize decoded = 0;
|
||||
// The host image is 256x256 with the same 7 levels: 256 down to 4.
|
||||
ASSERT_TRUE(Etc2ImageByteCounts(EtcFormat::kEtc2Rgba8, {64, 64, 1}, 7, 1, 4,
|
||||
&compressed, &decoded));
|
||||
EXPECT_EQ(compressed, 343U * 16U);
|
||||
EXPECT_EQ(decoded, 87376U * 4U);
|
||||
|
||||
EXPECT_FALSE(Etc2ImageByteCounts(EtcFormat::kEtc2Rgb8, {0, 4, 1}, 1, 1, 1,
|
||||
&compressed, &decoded));
|
||||
EXPECT_FALSE(Etc2ImageByteCounts(EtcFormat::kEtc2Rgb8, {4, 4, 1}, 0, 1, 1,
|
||||
&compressed, &decoded));
|
||||
}
|
||||
|
||||
TEST(VulkanEtc2EmulationTest, RejectsUnsupportedRegions) {
|
||||
VkDeviceSize compressed = 0;
|
||||
VkDeviceSize decoded = 0;
|
||||
@@ -147,6 +178,9 @@ VKAPI_ATTR VkResult VKAPI_CALL FakeCreateImage(VkDevice,
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL FakeDestroyImage(VkDevice, VkImage,
|
||||
const VkAllocationCallbacks*) {}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL FakeCopyImage(VkCommandBuffer, VkImage,
|
||||
VkImageLayout, VkImage, VkImageLayout,
|
||||
std::uint32_t, const VkImageCopy*) {
|
||||
@@ -295,6 +329,7 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL FakeGetDeviceProcAddr(
|
||||
if (is("vkDestroySemaphore"))
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(FakeDestroySemaphore);
|
||||
if (is("vkCreateImage")) return reinterpret_cast<PFN_vkVoidFunction>(FakeCreateImage);
|
||||
if (is("vkDestroyImage")) return reinterpret_cast<PFN_vkVoidFunction>(FakeDestroyImage);
|
||||
if (is("vkCreateBuffer")) return reinterpret_cast<PFN_vkVoidFunction>(FakeCreateBuffer);
|
||||
if (is("vkGetBufferMemoryRequirements"))
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(FakeBufferRequirements);
|
||||
@@ -313,6 +348,47 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL FakeGetDeviceProcAddr(
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TEST(VulkanEtc2EmulationTest, TotalsTheBytesOfLiveEmulatedImages) {
|
||||
const VkDevice device = FakeHandle<VkDevice>(0x12);
|
||||
VkPhysicalDeviceMemoryProperties memory{};
|
||||
VulkanEtc2Emulation emulation;
|
||||
emulation.RegisterDevice(device, FakeHandle<VkPhysicalDevice>(0x20), true,
|
||||
memory, FakeGetDeviceProcAddr);
|
||||
|
||||
VkImageCreateInfo image_info{};
|
||||
image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||
image_info.imageType = VK_IMAGE_TYPE_3D;
|
||||
image_info.format = VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK;
|
||||
image_info.extent = {8, 8, 1};
|
||||
image_info.mipLevels = 1;
|
||||
image_info.arrayLayers = 1;
|
||||
VkImage first = VK_NULL_HANDLE;
|
||||
VkImage second = VK_NULL_HANDLE;
|
||||
ASSERT_EQ(emulation.CreateImage(device, &image_info, nullptr, &first),
|
||||
VK_SUCCESS);
|
||||
ASSERT_EQ(emulation.CreateImage(device, &image_info, nullptr, &second),
|
||||
VK_SUCCESS);
|
||||
image_info.format = VK_FORMAT_R8G8B8A8_UNORM;
|
||||
VkImage plain = VK_NULL_HANDLE;
|
||||
ASSERT_EQ(emulation.CreateImage(device, &image_info, nullptr, &plain),
|
||||
VK_SUCCESS);
|
||||
|
||||
EmulatedTextureBytes totals = emulation.TextureBytes();
|
||||
EXPECT_EQ(totals.compressed, 2U * 4U * 8U);
|
||||
EXPECT_EQ(totals.decoded, 2U * 64U * 4U);
|
||||
|
||||
emulation.DestroyImage(device, first, nullptr);
|
||||
emulation.DestroyImage(device, plain, nullptr);
|
||||
totals = emulation.TextureBytes();
|
||||
EXPECT_EQ(totals.compressed, 4U * 8U);
|
||||
EXPECT_EQ(totals.decoded, 64U * 4U);
|
||||
|
||||
emulation.DestroyDevice(device);
|
||||
totals = emulation.TextureBytes();
|
||||
EXPECT_EQ(totals.compressed, 0U);
|
||||
EXPECT_EQ(totals.decoded, 0U);
|
||||
}
|
||||
|
||||
TEST(VulkanEtc2EmulationTest, SkipsUploadOutsideTheMappedWindow) {
|
||||
const VkDevice device = FakeHandle<VkDevice>(0x10);
|
||||
const VkBuffer source = FakeHandle<VkBuffer>(0x200);
|
||||
|
||||
Reference in new issue
Block a user