mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-18 19:00:48 +02:00
Compare commits
4
Commits
continuous
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4be3de14a | ||
|
|
876f8b8c32 | ||
|
|
c614fba032 | ||
|
|
046632cf99 |
No files matched your search
@@ -19,9 +19,19 @@ Release notes on GitHub are taken from the matching section here.
|
|||||||
maintains it.
|
maintains it.
|
||||||
- The log now names each Roblox Java call that Nightcap has no handler for, once per call, as
|
- 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.
|
`[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
|
### 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
|
- 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.
|
memory for each block of sound it plays.
|
||||||
|
|
||||||
|
|||||||
@@ -279,6 +279,7 @@ add_library(mocktail_runtime STATIC
|
|||||||
src/runtime/game_session_coordinator.cc
|
src/runtime/game_session_coordinator.cc
|
||||||
src/runtime/graphics_launch_policy.cc
|
src/runtime/graphics_launch_policy.cc
|
||||||
src/runtime/http_client_policy.cc
|
src/runtime/http_client_policy.cc
|
||||||
|
src/runtime/luau_codegen_switch.cc
|
||||||
src/runtime/memory_limit.cc
|
src/runtime/memory_limit.cc
|
||||||
src/runtime/owned_pthread.cc
|
src/runtime/owned_pthread.cc
|
||||||
src/runtime/performance_policy.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
|
PC profiles also set `FIntFlyoutTestVariantRollout12` to `2`, which shows the
|
||||||
desktop side panel menu. An explicit override takes precedence.
|
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
|
```json
|
||||||
{
|
{
|
||||||
"FFlagExample": "True",
|
"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
|
gives the number of cores busy. A long frame with little present-thread CPU
|
||||||
was spent waiting.
|
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
|
## The benchmark runs
|
||||||
|
|
||||||
Run every scenario before and after the change, on the same build type
|
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
|
## 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
|
smaller at N times their width and height. At the default that is 16 times the
|
||||||
pixels per affected texture.
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
## Frame rate and present mode
|
||||||
|
|
||||||
`graphics.frame_rate_limit` (default `-1`) leaves Roblox's own in-game
|
`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,
|
std::uint32_t layer_count, VkDeviceSize* compressed,
|
||||||
VkDeviceSize* decoded);
|
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
|
// Whether emulated ETC2 is reported to the application as a supported
|
||||||
// feature, from the MOCKTAIL_ADVERTISE_ETC2 value: "0" hides it, anything
|
// feature, from the MOCKTAIL_ADVERTISE_ETC2 value: "0" hides it, anything
|
||||||
// else (or unset) reports it. Hidden ETC2 still decodes ETC2 images the
|
// 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.
|
// pending, after waiting for any decode still writing into them.
|
||||||
void ReleaseCommandBuffer(VkCommandBuffer command_buffer);
|
void ReleaseCommandBuffer(VkCommandBuffer command_buffer);
|
||||||
|
|
||||||
|
// Traced as the "emulated texture (MB)" and "compressed texture (MB)"
|
||||||
|
// counters.
|
||||||
|
EmulatedTextureBytes TextureBytes() const;
|
||||||
|
|
||||||
struct State;
|
struct State;
|
||||||
|
|
||||||
private:
|
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_
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace mocktail {
|
namespace mocktail {
|
||||||
namespace runtime {
|
namespace runtime {
|
||||||
@@ -10,8 +11,10 @@ namespace runtime {
|
|||||||
// Android's RbxKeyboard resolves the Roblox Font enum through
|
// Android's RbxKeyboard resolves the Roblox Font enum through
|
||||||
// assets/android/fonts/font-mappings.json. The selected file lives below
|
// assets/android/fonts/font-mappings.json. The selected file lives below
|
||||||
// assets/content/fonts while legacy SourceSansPro faces live in assets/fonts.
|
// assets/content/fonts while legacy SourceSansPro faces live in assets/fonts.
|
||||||
|
// Emoji come from the color fonts Roblox ships next to the mapped faces.
|
||||||
struct RobloxTextFontSelection {
|
struct RobloxTextFontSelection {
|
||||||
std::string primary_file;
|
std::string primary_file;
|
||||||
|
std::vector<std::string> emoji_files;
|
||||||
float size_scale = 0.795F;
|
float size_scale = 0.795F;
|
||||||
bool exact_mapping = false;
|
bool exact_mapping = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
Usage: summarize_profile_trace.py TRACE [TRACE ...]
|
Usage: summarize_profile_trace.py TRACE [TRACE ...]
|
||||||
|
|
||||||
Each trace gets its own column, so `before.json after.json` compares them.
|
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
|
Frame times come from the "frame interval (ms)" counter, peaks from the
|
||||||
a slice name recorded by the Vulkan adapter.
|
texture counters; every other row is a slice name recorded by the Vulkan
|
||||||
|
adapter.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -20,6 +21,7 @@ import sys
|
|||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
FRAME_COUNTER = "frame interval (ms)"
|
FRAME_COUNTER = "frame interval (ms)"
|
||||||
|
PEAK_COUNTERS = ("emulated texture (MB)", "compressed texture (MB)")
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -37,6 +39,7 @@ class Distribution:
|
|||||||
class Summary:
|
class Summary:
|
||||||
frames: Distribution | None
|
frames: Distribution | None
|
||||||
slices: dict[str, Distribution]
|
slices: dict[str, Distribution]
|
||||||
|
peaks: dict[str, float]
|
||||||
|
|
||||||
|
|
||||||
def _nearest_rank(ordered: list[float], percent: float) -> 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:
|
def summarize(events: Iterable[dict]) -> Summary:
|
||||||
frames: list[float] = []
|
frames: list[float] = []
|
||||||
slices: dict[str, list[float]] = {}
|
slices: dict[str, list[float]] = {}
|
||||||
|
peaks: dict[str, float] = {}
|
||||||
for event in events:
|
for event in events:
|
||||||
phase = event.get("ph")
|
phase = event.get("ph")
|
||||||
if phase == "C" and event.get("name") == FRAME_COUNTER:
|
if phase == "C" and event.get("name") == FRAME_COUNTER:
|
||||||
frames.append(float(event["args"]["value"]))
|
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":
|
elif phase == "X":
|
||||||
slices.setdefault(event["name"], []).append(event["dur"] / 1000)
|
slices.setdefault(event["name"], []).append(event["dur"] / 1000)
|
||||||
return Summary(
|
return Summary(
|
||||||
frames=_distribution(frames) if frames else None,
|
frames=_distribution(frames) if frames else None,
|
||||||
slices={name: _distribution(values)
|
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:
|
def _row(label: str, cells: list[str]) -> str:
|
||||||
@@ -96,6 +104,11 @@ def to_markdown(traces: list[tuple[str, Summary]]) -> str:
|
|||||||
if any(frames):
|
if any(frames):
|
||||||
lines += [_metric_row(f"frame interval {field} (ms)", frames, field)
|
lines += [_metric_row(f"frame interval {field} (ms)", frames, field)
|
||||||
for field in ("mean", "p50", "p95", "p99", "max")]
|
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.
|
# Largest total time first, taken from whichever trace spent the most.
|
||||||
totals: dict[str, float] = {}
|
totals: dict[str, float] = {}
|
||||||
|
|||||||
@@ -92,6 +92,34 @@ bool Etc2UploadByteCounts(EtcFormat format, const VkExtent3D& extent,
|
|||||||
return true;
|
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 {
|
namespace {
|
||||||
|
|
||||||
bool EmulationDisabledByEnvironment() {
|
bool EmulationDisabledByEnvironment() {
|
||||||
@@ -207,8 +235,21 @@ struct ImageRecord {
|
|||||||
// Decoded level-0 texels of a scaled image; every host mip is resampled
|
// 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.
|
// from them so the texture keeps its detail at every draw size.
|
||||||
std::shared_ptr<const RgbaImage> base;
|
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) {
|
bool IsColorFormat(EtcFormat format) {
|
||||||
return format == EtcFormat::kEtc2Rgb8 || format == EtcFormat::kEtc2Rgb8A1 ||
|
return format == EtcFormat::kEtc2Rgb8 || format == EtcFormat::kEtc2Rgb8A1 ||
|
||||||
format == EtcFormat::kEtc2Rgba8;
|
format == EtcFormat::kEtc2Rgba8;
|
||||||
@@ -669,6 +710,8 @@ struct VulkanEtc2Emulation::State {
|
|||||||
std::unordered_map<VkPhysicalDevice, bool> physical_devices;
|
std::unordered_map<VkPhysicalDevice, bool> physical_devices;
|
||||||
std::vector<std::unique_ptr<HostDevice>> devices;
|
std::vector<std::unique_ptr<HostDevice>> devices;
|
||||||
std::unordered_map<VkImage, ImageRecord> images;
|
std::unordered_map<VkImage, ImageRecord> images;
|
||||||
|
// Sum over `images`; guarded by `mutex`.
|
||||||
|
EmulatedTextureBytes texture_bytes;
|
||||||
std::unordered_map<VkBuffer, BufferBinding> buffers;
|
std::unordered_map<VkBuffer, BufferBinding> buffers;
|
||||||
std::unordered_map<VkDeviceMemory, Mapping> mappings;
|
std::unordered_map<VkDeviceMemory, Mapping> mappings;
|
||||||
std::unordered_map<VkCommandBuffer, CommandRecord> commands;
|
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.
|
// Nothing may free staging or the semaphore while a decode still writes it.
|
||||||
state_->DrainDecodes();
|
state_->DrainDecodes();
|
||||||
std::vector<std::unique_ptr<StagingBlock>> doomed;
|
std::vector<std::unique_ptr<StagingBlock>> doomed;
|
||||||
|
EmulatedTextureBytes totals;
|
||||||
|
std::uint64_t now = 0;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||||
for (auto it = state_->commands.begin(); it != state_->commands.end();) {
|
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();) {
|
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(),
|
state_->has_commands.store(!state_->commands.empty(),
|
||||||
std::memory_order_release);
|
std::memory_order_release);
|
||||||
}
|
}
|
||||||
|
TraceTextureBytes(totals, now);
|
||||||
if (const HostDevice* dev = state_->Find(device); dev != nullptr) {
|
if (const HostDevice* dev = state_->Find(device); dev != nullptr) {
|
||||||
for (const auto& block : doomed) {
|
for (const auto& block : doomed) {
|
||||||
DestroyStagingBlock(*dev, *block);
|
DestroyStagingBlock(*dev, *block);
|
||||||
@@ -1316,6 +1370,14 @@ VkResult VulkanEtc2Emulation::CreateImage(VkDevice device,
|
|||||||
}
|
}
|
||||||
const VkResult result = dev->create_image(device, &host_info, allocator, image);
|
const VkResult result = dev->create_image(device, &host_info, allocator, image);
|
||||||
if (result == VK_SUCCESS && image != nullptr) {
|
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);
|
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||||
state_->images[*image] = {device,
|
state_->images[*image] = {device,
|
||||||
@@ -1324,8 +1386,14 @@ VkResult VulkanEtc2Emulation::CreateImage(VkDevice device,
|
|||||||
create_info->extent.width,
|
create_info->extent.width,
|
||||||
create_info->extent.height,
|
create_info->extent.height,
|
||||||
nullptr,
|
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)) {
|
if (ShouldLog(&g_image_logs, 4)) {
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
" [vulkan] ETC2 image %ux%u mips=%u format=%d -> host "
|
" [vulkan] ETC2 image %ux%u mips=%u format=%d -> host "
|
||||||
@@ -1346,12 +1414,33 @@ void VulkanEtc2Emulation::DestroyImage(VkDevice device, VkImage image,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (dev->emulated) {
|
if (dev->emulated) {
|
||||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
EmulatedTextureBytes totals;
|
||||||
state_->images.erase(image);
|
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);
|
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(
|
VkResult VulkanEtc2Emulation::CreateImageView(
|
||||||
VkDevice device, const VkImageViewCreateInfo* create_info,
|
VkDevice device, const VkImageViewCreateInfo* create_info,
|
||||||
const VkAllocationCallbacks* allocator, VkImageView* view) {
|
const VkAllocationCallbacks* allocator, VkImageView* view) {
|
||||||
|
|||||||
@@ -66,6 +66,7 @@
|
|||||||
#include "runtime/display_size.h"
|
#include "runtime/display_size.h"
|
||||||
#include "runtime/environment.h"
|
#include "runtime/environment.h"
|
||||||
#include "runtime/discord_rpc.h"
|
#include "runtime/discord_rpc.h"
|
||||||
|
#include "runtime/luau_codegen_switch.h"
|
||||||
#include "runtime/jnivm_platform_web_callbacks.h"
|
#include "runtime/jnivm_platform_web_callbacks.h"
|
||||||
#include "runtime/owned_pthread.h"
|
#include "runtime/owned_pthread.h"
|
||||||
#include "runtime/platform_cache_migration.h"
|
#include "runtime/platform_cache_migration.h"
|
||||||
@@ -4656,6 +4657,27 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
|||||||
<< "; continuing without Rich Presence\n";
|
<< "; 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 =
|
const mocktail::runtime::InputCapabilityConfig& input_capabilities =
|
||||||
runtime_config.input_capabilities();
|
runtime_config.input_capabilities();
|
||||||
if (!runtime_config.frame_rate().valid()) {
|
if (!runtime_config.frame_rate().valid()) {
|
||||||
@@ -6834,7 +6856,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
|||||||
jni_factory, present_boundary, std::move(surface_config),
|
jni_factory, present_boundary, std::move(surface_config),
|
||||||
&dependencies.roblox_credential(),
|
&dependencies.roblox_credential(),
|
||||||
mocktail::runtime::RobloxExperienceSurfaceProvider{},
|
mocktail::runtime::RobloxExperienceSurfaceProvider{},
|
||||||
discord_rpc.observer(),
|
luau_codegen_switch.observer(),
|
||||||
dependencies.clear_persisted_web_view_cookie(),
|
dependencies.clear_persisted_web_view_cookie(),
|
||||||
runtime_config.microphone_enabled(),
|
runtime_config.microphone_enabled(),
|
||||||
platform_web_symbols.account);
|
platform_web_symbols.account);
|
||||||
|
|||||||
+10
-3
@@ -33,6 +33,7 @@
|
|||||||
#include "runtime/fleasion.h"
|
#include "runtime/fleasion.h"
|
||||||
#include "runtime/game_mode.h"
|
#include "runtime/game_mode.h"
|
||||||
#include "runtime/graphics_launch_policy.h"
|
#include "runtime/graphics_launch_policy.h"
|
||||||
|
#include "runtime/luau_codegen_switch.h"
|
||||||
#include "runtime/memory_limit.h"
|
#include "runtime/memory_limit.h"
|
||||||
#include "runtime/payload_update_preflight.h"
|
#include "runtime/payload_update_preflight.h"
|
||||||
#include "runtime/performance_policy.h"
|
#include "runtime/performance_policy.h"
|
||||||
@@ -1012,6 +1013,7 @@ int main(int argc, char* argv[]) {
|
|||||||
std::cout
|
std::cout
|
||||||
<< " [auth] explicit guest identity selected for production VM\n";
|
<< " [auth] explicit guest identity selected for production VM\n";
|
||||||
}
|
}
|
||||||
|
unsetenv("MOCKTAIL_LUAU_CODEGEN_MENU_SWITCH");
|
||||||
if (runtime_config.config.desktop_playability()) {
|
if (runtime_config.config.desktop_playability()) {
|
||||||
std::filesystem::path assets = paths.DefaultAssetPath();
|
std::filesystem::path assets = paths.DefaultAssetPath();
|
||||||
if (environment.HasNonEmpty("MOCKTAIL_ASSET_PATH")) {
|
if (environment.HasNonEmpty("MOCKTAIL_ASSET_PATH")) {
|
||||||
@@ -1034,10 +1036,13 @@ int main(int argc, char* argv[]) {
|
|||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
std::string desktop_client_settings;
|
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(
|
if (!mocktail::runtime::MergeDesktopAppPolicyClientSettingsOverride(
|
||||||
desktop_policy.policy_json,
|
desktop_policy.policy_json, user_client_settings,
|
||||||
environment.GetOr("MOCKTAIL_CLIENT_SETTINGS_OVERRIDES_JSON",
|
|
||||||
"{}"),
|
|
||||||
&desktop_client_settings, &command_line_error)) {
|
&desktop_client_settings, &command_line_error)) {
|
||||||
std::cerr << "[FATAL] Cannot activate desktop Roblox app-policy: "
|
std::cerr << "[FATAL] Cannot activate desktop Roblox app-policy: "
|
||||||
<< command_line_error << '\n';
|
<< command_line_error << '\n';
|
||||||
@@ -1048,6 +1053,8 @@ int main(int argc, char* argv[]) {
|
|||||||
std::cerr << "[FATAL] Cannot export desktop Roblox app-policy\n";
|
std::cerr << "[FATAL] Cannot export desktop Roblox app-policy\n";
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
setenv("MOCKTAIL_LUAU_CODEGEN_MENU_SWITCH",
|
||||||
|
user_set_luau_codegen ? "0" : "1", 1);
|
||||||
std::cout << " [runtime] desktop app-policy ready: normalized="
|
std::cout << " [runtime] desktop app-policy ready: normalized="
|
||||||
<< desktop_policy.normalized_policy_count
|
<< desktop_policy.normalized_policy_count
|
||||||
<< " updated=" << (desktop_policy.updated ? 1 : 0)
|
<< " 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 <string_view>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#include "runtime/luau_codegen_switch.h"
|
||||||
#include "runtime/runtime_paths.h"
|
#include "runtime/runtime_paths.h"
|
||||||
|
|
||||||
namespace mocktail {
|
namespace mocktail {
|
||||||
@@ -357,6 +358,9 @@ bool MergeDesktopAppPolicyClientSettingsOverride(std::string_view policy_json,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
overrides[std::string(kDesktopAppPolicyOverride)] = policy_json;
|
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
|
// 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
|
// leave the whole LuaApp blank. Default to the working legacy page while
|
||||||
// preserving explicit user overrides for testing newer UI patches.
|
// preserving explicit user overrides for testing newer UI patches.
|
||||||
|
|||||||
@@ -1446,8 +1446,9 @@ Status RobloxExperienceComposition::DrainLaunchRequests() {
|
|||||||
return Status::Ok();
|
return Status::Ok();
|
||||||
if (!launch_in_progress_ && active_launch_ == nullptr && game_active_ &&
|
if (!launch_in_progress_ && active_launch_ == nullptr && game_active_ &&
|
||||||
!pending_launch_requests_.empty()) {
|
!pending_launch_requests_.empty()) {
|
||||||
// Start the queued launch only after consuming this leave callback, so
|
// Blocks a reentrant drain while the leave pumps main-thread messages.
|
||||||
// it cannot reach the next controller.
|
// 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;
|
controlled_switch_waiting_for_return_ = true;
|
||||||
switch_active_game = true;
|
switch_active_game = true;
|
||||||
}
|
}
|
||||||
@@ -1456,7 +1457,7 @@ Status RobloxExperienceComposition::DrainLaunchRequests() {
|
|||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"[experience] leaving active game for queued launch\n");
|
"[experience] leaving active game for queued launch\n");
|
||||||
const Status leave_status = LeaveGame();
|
const Status leave_status = LeaveGame();
|
||||||
if (!leave_status.ok()) {
|
{
|
||||||
std::lock_guard<std::mutex> lock(mutex_);
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
controlled_switch_waiting_for_return_ = false;
|
controlled_switch_waiting_for_return_ = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace mocktail {
|
namespace mocktail {
|
||||||
namespace runtime {
|
namespace runtime {
|
||||||
@@ -101,6 +102,24 @@ std::string LegacyFontName(int32_t font) {
|
|||||||
return "SourceSansPro-Regular.ttf";
|
return "SourceSansPro-Regular.ttf";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TwemojiMozilla covers Unicode emoji; RobloxEmoji holds the private-use
|
||||||
|
// Roblox and Robux symbols.
|
||||||
|
constexpr const char* kEmojiFontNames[] = {"TwemojiMozilla.ttf",
|
||||||
|
"RobloxEmoji.ttf"};
|
||||||
|
|
||||||
|
std::vector<std::string> EmojiFontFiles(
|
||||||
|
const std::filesystem::path& asset_content_root) {
|
||||||
|
std::vector<std::string> files;
|
||||||
|
for (const char* name : kEmojiFontNames) {
|
||||||
|
const std::filesystem::path candidate =
|
||||||
|
asset_content_root / "fonts" / name;
|
||||||
|
if (IsRegularFile(candidate)) {
|
||||||
|
files.push_back(candidate.lexically_normal().string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
RobloxTextFontSelection ResolveRobloxTextFont(
|
RobloxTextFontSelection ResolveRobloxTextFont(
|
||||||
@@ -111,6 +130,7 @@ RobloxTextFontSelection ResolveRobloxTextFont(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const std::filesystem::path content(asset_content_root);
|
const std::filesystem::path content(asset_content_root);
|
||||||
|
selection.emoji_files = EmojiFontFiles(content);
|
||||||
const std::optional<FontMapping> mapped = MappingFor(font, content);
|
const std::optional<FontMapping> mapped = MappingFor(font, content);
|
||||||
if (mapped.has_value()) {
|
if (mapped.has_value()) {
|
||||||
const std::filesystem::path candidate = content / "fonts" / mapped->file;
|
const std::filesystem::path candidate = content / "fonts" / mapped->file;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ namespace {
|
|||||||
|
|
||||||
constexpr std::size_t kMaximumRasterTextBytes = 4096;
|
constexpr std::size_t kMaximumRasterTextBytes = 4096;
|
||||||
constexpr std::uint64_t kMaximumOverlayPixels = 16ULL * 1024ULL * 1024ULL;
|
constexpr std::uint64_t kMaximumOverlayPixels = 16ULL * 1024ULL * 1024ULL;
|
||||||
constexpr std::size_t kMaximumFallbackFonts = 6;
|
constexpr std::size_t kMaximumFallbackFonts = 8;
|
||||||
|
|
||||||
std::mutex g_active_overlay_mutex;
|
std::mutex g_active_overlay_mutex;
|
||||||
RobloxTextSurfaceOverlay* g_active_overlay = nullptr;
|
RobloxTextSurfaceOverlay* g_active_overlay = nullptr;
|
||||||
@@ -121,10 +121,35 @@ std::vector<std::string> ResolveFontFiles(
|
|||||||
} else if (!selection.primary_file.empty()) {
|
} else if (!selection.primary_file.empty()) {
|
||||||
files.push_back(selection.primary_file);
|
files.push_back(selection.primary_file);
|
||||||
}
|
}
|
||||||
|
files.insert(files.end(), selection.emoji_files.begin(),
|
||||||
|
selection.emoji_files.end());
|
||||||
if (!FcInit()) {
|
if (!FcInit()) {
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The sorted sans list rarely reaches an emoji font before the cap, so ask
|
||||||
|
// for the system emoji font by name ahead of it.
|
||||||
|
FcPattern* emoji = FcNameParse(reinterpret_cast<const FcChar8*>("emoji"));
|
||||||
|
if (emoji != nullptr) {
|
||||||
|
FcConfigSubstitute(nullptr, emoji, FcMatchPattern);
|
||||||
|
FcDefaultSubstitute(emoji);
|
||||||
|
FcResult emoji_result = FcResultNoMatch;
|
||||||
|
FcPattern* matched = FcFontMatch(nullptr, emoji, &emoji_result);
|
||||||
|
FcPatternDestroy(emoji);
|
||||||
|
FcChar8* file = nullptr;
|
||||||
|
if (matched != nullptr &&
|
||||||
|
FcPatternGetString(matched, FC_FILE, 0, &file) == FcResultMatch &&
|
||||||
|
file != nullptr && file[0] != '\0') {
|
||||||
|
const std::string candidate(reinterpret_cast<const char*>(file));
|
||||||
|
if (std::find(files.begin(), files.end(), candidate) == files.end()) {
|
||||||
|
files.push_back(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matched != nullptr) {
|
||||||
|
FcPatternDestroy(matched);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FcPattern* pattern = FcNameParse(reinterpret_cast<const FcChar8*>("sans"));
|
FcPattern* pattern = FcNameParse(reinterpret_cast<const FcChar8*>("sans"));
|
||||||
if (pattern == nullptr) {
|
if (pattern == nullptr) {
|
||||||
return files;
|
return files;
|
||||||
|
|||||||
@@ -279,6 +279,13 @@ target_link_libraries(roblox_desktop_app_policy_test PRIVATE
|
|||||||
)
|
)
|
||||||
mocktail_apply_compile_options(roblox_desktop_app_policy_test)
|
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)
|
add_executable(browser_tracker_service_test browser_tracker_service_test.cc)
|
||||||
target_link_libraries(browser_tracker_service_test PRIVATE
|
target_link_libraries(browser_tracker_service_test PRIVATE
|
||||||
Mocktail::Services
|
Mocktail::Services
|
||||||
@@ -1143,6 +1150,7 @@ if(MOCKTAIL_ENABLE_UPSTREAM_JNIVM)
|
|||||||
endif()
|
endif()
|
||||||
gtest_discover_tests(libc_shim_test)
|
gtest_discover_tests(libc_shim_test)
|
||||||
gtest_discover_tests(discord_rpc_test)
|
gtest_discover_tests(discord_rpc_test)
|
||||||
|
gtest_discover_tests(luau_codegen_switch_test)
|
||||||
gtest_discover_tests(asset_manager_test)
|
gtest_discover_tests(asset_manager_test)
|
||||||
gtest_discover_tests(android_looper_test)
|
gtest_discover_tests(android_looper_test)
|
||||||
gtest_discover_tests(android_native_window_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");
|
"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) {
|
TEST(RobloxDesktopAppPolicyTest, RejectsMalformedOrSymlinkedStorage) {
|
||||||
TemporaryDirectory temporary;
|
TemporaryDirectory temporary;
|
||||||
const std::filesystem::path default_path = temporary.path() / "default.json";
|
const std::filesystem::path default_path = temporary.path() / "default.json";
|
||||||
|
|||||||
@@ -816,13 +816,9 @@ TEST(RobloxExperienceCompositionTest,
|
|||||||
ASSERT_TRUE(composition.DrainLaunchRequests().ok());
|
ASSERT_TRUE(composition.DrainLaunchRequests().ok());
|
||||||
EXPECT_EQ(probe.leaves, 1);
|
EXPECT_EQ(probe.leaves, 1);
|
||||||
EXPECT_EQ(probe.starts.load(), 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
|
// nativeAppBridgeV2LeaveGame ends in the Native stage and Roblox sends no
|
||||||
// controller and the replacement server launch.
|
// Lua app return, so the queued launch starts once the leave has returned.
|
||||||
composition.NotifyLuaAppDidReturn();
|
|
||||||
for (int attempt = 0; attempt < 1000 && probe.starts.load() < 2; ++attempt) {
|
for (int attempt = 0; attempt < 1000 && probe.starts.load() < 2; ++attempt) {
|
||||||
ASSERT_TRUE(composition.DrainLaunchRequests().ok());
|
ASSERT_TRUE(composition.DrainLaunchRequests().ok());
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||||
|
|||||||
@@ -97,6 +97,23 @@ TEST(RobloxTextFontResolverTest, RejectsMappingPathTraversal) {
|
|||||||
(assets.root() / "assets/fonts/SourceSansPro-Regular.ttf").string());
|
(assets.root() / "assets/fonts/SourceSansPro-Regular.ttf").string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(RobloxTextFontResolverTest, AddsBundledEmojiFontsAsFallbacks) {
|
||||||
|
TemporaryFontAssets assets;
|
||||||
|
ASSERT_FALSE(assets.root().empty());
|
||||||
|
ASSERT_TRUE(assets.CreateFile("assets/fonts/SourceSansPro-Regular.ttf"));
|
||||||
|
ASSERT_TRUE(assets.CreateFile("assets/content/fonts/RobloxEmoji.ttf"));
|
||||||
|
ASSERT_TRUE(assets.CreateFile("assets/content/fonts/TwemojiMozilla.ttf"));
|
||||||
|
|
||||||
|
const RobloxTextFontSelection selection =
|
||||||
|
ResolveRobloxTextFont(3, assets.content().string());
|
||||||
|
|
||||||
|
ASSERT_EQ(selection.emoji_files.size(), 2U);
|
||||||
|
EXPECT_EQ(selection.emoji_files[0],
|
||||||
|
(assets.content() / "fonts/TwemojiMozilla.ttf").string());
|
||||||
|
EXPECT_EQ(selection.emoji_files[1],
|
||||||
|
(assets.content() / "fonts/RobloxEmoji.ttf").string());
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace runtime
|
} // namespace runtime
|
||||||
} // namespace mocktail
|
} // namespace mocktail
|
||||||
@@ -89,6 +89,20 @@ class SummarizeTest(unittest.TestCase):
|
|||||||
self.assertIn("| frame interval p50 (ms) | 20.00 | 10.00 |", lines)
|
self.assertIn("| frame interval p50 (ms) | 20.00 | 10.00 |", lines)
|
||||||
self.assertIn("| a mean (ms) | 1.00 | 0.50 |", 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:
|
def test_markdown_marks_missing_values(self) -> None:
|
||||||
with_slice = summary.summarize([slice_event("a", 1_000)])
|
with_slice = summary.summarize([slice_event("a", 1_000)])
|
||||||
without = summary.summarize([])
|
without = summary.summarize([])
|
||||||
|
|||||||
@@ -102,6 +102,37 @@ TEST(VulkanEtc2EmulationTest, CountsUploadBytesPerRegion) {
|
|||||||
EXPECT_EQ(decoded, 2U);
|
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) {
|
TEST(VulkanEtc2EmulationTest, RejectsUnsupportedRegions) {
|
||||||
VkDeviceSize compressed = 0;
|
VkDeviceSize compressed = 0;
|
||||||
VkDeviceSize decoded = 0;
|
VkDeviceSize decoded = 0;
|
||||||
@@ -147,6 +178,9 @@ VKAPI_ATTR VkResult VKAPI_CALL FakeCreateImage(VkDevice,
|
|||||||
return VK_SUCCESS;
|
return VK_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FakeDestroyImage(VkDevice, VkImage,
|
||||||
|
const VkAllocationCallbacks*) {}
|
||||||
|
|
||||||
VKAPI_ATTR void VKAPI_CALL FakeCopyImage(VkCommandBuffer, VkImage,
|
VKAPI_ATTR void VKAPI_CALL FakeCopyImage(VkCommandBuffer, VkImage,
|
||||||
VkImageLayout, VkImage, VkImageLayout,
|
VkImageLayout, VkImage, VkImageLayout,
|
||||||
std::uint32_t, const VkImageCopy*) {
|
std::uint32_t, const VkImageCopy*) {
|
||||||
@@ -295,6 +329,7 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL FakeGetDeviceProcAddr(
|
|||||||
if (is("vkDestroySemaphore"))
|
if (is("vkDestroySemaphore"))
|
||||||
return reinterpret_cast<PFN_vkVoidFunction>(FakeDestroySemaphore);
|
return reinterpret_cast<PFN_vkVoidFunction>(FakeDestroySemaphore);
|
||||||
if (is("vkCreateImage")) return reinterpret_cast<PFN_vkVoidFunction>(FakeCreateImage);
|
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("vkCreateBuffer")) return reinterpret_cast<PFN_vkVoidFunction>(FakeCreateBuffer);
|
||||||
if (is("vkGetBufferMemoryRequirements"))
|
if (is("vkGetBufferMemoryRequirements"))
|
||||||
return reinterpret_cast<PFN_vkVoidFunction>(FakeBufferRequirements);
|
return reinterpret_cast<PFN_vkVoidFunction>(FakeBufferRequirements);
|
||||||
@@ -313,6 +348,47 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL FakeGetDeviceProcAddr(
|
|||||||
return nullptr;
|
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) {
|
TEST(VulkanEtc2EmulationTest, SkipsUploadOutsideTheMappedWindow) {
|
||||||
const VkDevice device = FakeHandle<VkDevice>(0x10);
|
const VkDevice device = FakeHandle<VkDevice>(0x10);
|
||||||
const VkBuffer source = FakeHandle<VkBuffer>(0x200);
|
const VkBuffer source = FakeHandle<VkBuffer>(0x200);
|
||||||
|
|||||||
Reference in new issue
Block a user