Skip to content

Commit 8e2fa5a

Browse files
committed
Merge branch 'chore/visual-test-app-args' of github.com:decentraland/unity-explorer into test/visual-tests
2 parents aabdfe5 + 3b47ddb commit 8e2fa5a

7 files changed

Lines changed: 132 additions & 15 deletions

File tree

Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public static class AppArgsFlags
5757
public const string CREATOR_HUB_BIN_PATH = "creator-hub-bin-path";
5858

5959
public const string USE_LOG_MATRIX = "use-log-matrix";
60+
public const string GRAPHICS = "graphics";
6061
public const string WINDOWED_MODE = "windowed-mode";
6162
public const string RESOLUTION = "resolution";
6263
public const string DISABLE_WINDOW_RESTRICTIONS = "disable-window-restrictions";

Explorer/Assets/DCL/Infrastructure/Global/Dynamic/Bootstraper.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22
using CommunicationData.URLHelpers;
33
using Cysharp.Threading.Tasks;
44
using DCL.Audio;
5-
using DCL.CharacterCamera;
65
using DCL.Chat.History;
76
using DCL.DebugUtilities;
87
using DCL.Diagnostics;
98
using DCL.FeatureFlags;
10-
using DCL.InWorldCamera;
119
using DCL.Multiplayer.Connections.DecentralandUrls;
1210
using DCL.Notifications.NewNotification;
1311
using DCL.Optimization.PerformanceBudgeting;
@@ -352,31 +350,32 @@ await dynamicWorldContainer.UserInAppInAppInitializationFlow.ExecuteAsync(
352350
playerEntity: playerEntity
353351
), ct);
354352

355-
OpenDefaultUI(dynamicWorldContainer.MvcManager, globalWorld.EcsWorld, ct);
353+
OpenDefaultUI(dynamicWorldContainer.MvcManager, ct);
356354

357355
splashScreen.Hide();
358356
}
359357

360-
private void OpenDefaultUI(IMVCManager mvcManager, World ecsWorld, CancellationToken ct)
358+
private void OpenDefaultUI(IMVCManager mvcManager, CancellationToken ct)
361359
{
362360
mvcManager.ShowAsync(NewNotificationController.IssueCommand(), ct).Forget();
363361
mvcManager.ShowAsync(MainUIController.IssueCommand(), ct).Forget();
364362

365363
if (appArgs.HasFlag(AppArgsFlags.DISABLE_HUD))
366-
DisableHudOnStartupAsync(ecsWorld, ct).Forget();
364+
DisableHudOnStartupAsync(mvcManager, ct).Forget();
367365
}
368366

369-
private static async UniTaskVoid DisableHudOnStartupAsync(World ecsWorld, CancellationToken ct)
367+
internal static async UniTask DisableHudOnStartupAsync(IMVCManager mvcManager, CancellationToken ct)
370368
{
371369
try
372370
{
373-
// Wait a frame: MVC views are lazy; SetViewCanvasActive no-ops until viewFactory has run.
371+
// Wait a frame so lazily-mounted MVC views exist before toggling.
374372
await UniTask.NextFrame(ct).SuppressCancellationThrow();
375373

376374
if (ct.IsCancellationRequested)
377375
return;
378376

379-
ecsWorld.Add(ecsWorld.CacheCamera(), new ToggleUIRequest { Enable = false, Except = null });
377+
// Bypass ToggleUIRequest (used by U key) so scene SDK UIDocuments stay visible.
378+
mvcManager.SetAllViewsCanvasActive(false);
380379
}
381380
catch (Exception e) { ReportHub.LogException(e, ReportCategory.STARTUP); }
382381
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using Cysharp.Threading.Tasks;
2+
using Global.Dynamic;
3+
using MVC;
4+
using NSubstitute;
5+
using NUnit.Framework;
6+
using System.Collections;
7+
using System.Threading;
8+
using UnityEngine.TestTools;
9+
10+
namespace Global.Tests.EditMode
11+
{
12+
public class BootstraperShould
13+
{
14+
[UnityTest]
15+
public IEnumerator DisableHudOnStartup_OnlyTouchesMVCCanvases_NotSceneUIDocuments()
16+
{
17+
IMVCManager mvcManager = Substitute.For<IMVCManager>();
18+
19+
yield return Bootstrap.DisableHudOnStartupAsync(mvcManager, CancellationToken.None).ToCoroutine();
20+
21+
mvcManager.Received(1).SetAllViewsCanvasActive(false);
22+
mvcManager.DidNotReceiveWithAnyArgs().SetAllViewsCanvasActive(default(IController), default);
23+
}
24+
25+
[UnityTest]
26+
public IEnumerator DisableHudOnStartup_DoesNothing_WhenCancelledBeforeFrameAdvances()
27+
{
28+
IMVCManager mvcManager = Substitute.For<IMVCManager>();
29+
var cts = new CancellationTokenSource();
30+
cts.Cancel();
31+
32+
yield return Bootstrap.DisableHudOnStartupAsync(mvcManager, cts.Token).ToCoroutine();
33+
34+
mvcManager.DidNotReceiveWithAnyArgs().SetAllViewsCanvasActive(default);
35+
}
36+
}
37+
}

Explorer/Assets/DCL/Infrastructure/Global/Tests/EditMode/BootstraperShould.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Explorer/Assets/DCL/Quality/Runtime/QualitySettingsController.cs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ public QualitySettingsController(
6262
this.appArgs = appArgs;
6363
this.analytics = analytics;
6464

65+
if (SavedQualitySettingsApplier.TryGetPresetOverride(appArgs, out QualityPresetLevel overridePreset))
66+
{
67+
ApplyPresetInternal(overridePreset, persist: false);
68+
return;
69+
}
70+
6571
QualityPresetLevel savedPreset = SavedQualitySettingsApplier.ReadSavedPreset();
6672

6773
if (savedPreset == QualityPresetLevel.Custom)
@@ -73,7 +79,10 @@ public QualitySettingsController(
7379
else { SetPreset(savedPreset); }
7480
}
7581

76-
public void SetPreset(QualityPresetLevel level)
82+
public void SetPreset(QualityPresetLevel level) =>
83+
ApplyPresetInternal(level, persist: true);
84+
85+
private void ApplyPresetInternal(QualityPresetLevel level, bool persist)
7786
{
7887
if (level == QualityPresetLevel.Custom) { throw new ArgumentException("Cannot set custom preset from QualitySettingsController"); }
7988

@@ -85,9 +94,11 @@ public void SetPreset(QualityPresetLevel level)
8594
CurrentPreset = level;
8695
presetData = preset;
8796

88-
DCLPlayerPrefs.SetInt(DCLPrefKeys.PS_QUALITY_PRESET, EnumUtils.ToInt(level));
89-
90-
DeleteCustomSettings();
97+
if (persist)
98+
{
99+
DCLPlayerPrefs.SetInt(DCLPrefKeys.PS_QUALITY_PRESET, EnumUtils.ToInt(level));
100+
DeleteCustomSettings();
101+
}
91102

92103
FpsLimit = preset.FpsLimit;
93104
VSync = preset.VSyncEnabled;
@@ -107,11 +118,14 @@ public void SetPreset(QualityPresetLevel level)
107118
ShadowDistance = preset.ShadowDistance;
108119
PlayCurrentSceneStreamsOnly = preset.PlayCurrentSceneStreamsOnly;
109120

110-
ApplyAllSettings();
121+
ApplyAllSettings(persist);
111122
OnPresetChanged?.Invoke(level);
112123
}
113124

114-
public void ApplyAllSettings()
125+
public void ApplyAllSettings() =>
126+
ApplyAllSettings(persist: true);
127+
128+
private void ApplyAllSettings(bool persist)
115129
{
116130
URPSettingsApplier.ApplyVSync(VSync, FpsLimit);
117131
upscalingController.UpdateUpscaling(ResolutionScale);
@@ -125,7 +139,7 @@ public void ApplyAllSettings()
125139
landscapeData.DetailDistance = LandscapeDistance;
126140

127141
URPSettingsApplier.ApplySunShadows(SunShadows);
128-
DCLPlayerPrefs.SetBool(DCLPrefKeys.PS_SUN_LENS_FLARE, SunLensFlare);
142+
if (persist) DCLPlayerPrefs.SetBool(DCLPrefKeys.PS_SUN_LENS_FLARE, SunLensFlare);
129143
URPSettingsApplier.ApplySunLensFlare(SunLensFlare);
130144
URPSettingsApplier.ApplySceneLight(SceneLights);
131145
URPSettingsApplier.ApplyMaxObjectsPerLight(MaxSceneLights);

Explorer/Assets/DCL/Quality/Runtime/SavedQualitySettingsApplier.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
using DCL.Diagnostics;
12
using DCL.Prefs;
3+
using Global.AppArgs;
4+
using System;
25
using Utility;
36

47
namespace DCL.Quality.Runtime
@@ -8,6 +11,26 @@ namespace DCL.Quality.Runtime
811
/// </summary>
912
public static class SavedQualitySettingsApplier
1013
{
14+
/// <summary>
15+
/// Reads a quality preset override from app arguments (e.g. --graphics high).
16+
/// Custom is rejected — only Low/Medium/High are valid.
17+
/// </summary>
18+
public static bool TryGetPresetOverride(IAppArgs appArgs, out QualityPresetLevel preset)
19+
{
20+
preset = default;
21+
22+
if (!appArgs.TryGetValue(AppArgsFlags.GRAPHICS, out string? value) || string.IsNullOrEmpty(value))
23+
return false;
24+
25+
if (!Enum.TryParse(value, ignoreCase: true, out preset) || preset == QualityPresetLevel.Custom)
26+
{
27+
ReportHub.LogWarning(ReportCategory.SETTINGS_MENU, $"Invalid value for --{AppArgsFlags.GRAPHICS}: '{value}'. Expected Low, Medium or High.");
28+
return false;
29+
}
30+
31+
return true;
32+
}
33+
1134
public struct SavedValues
1235
{
1336
public int FpsLimit;

docs/app-arguments.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,19 @@ More detailed instructions on how to test can be found in the description of rel
241241

242242
---
243243

244+
### `graphics`
245+
**Type:** String (`Low`, `Medium`, or `High`, case-insensitive)
246+
**Description:** Forces a graphics quality preset on startup, overriding whatever preset is saved in PlayerPrefs. The override is ephemeral — PlayerPrefs are not modified, so launching again without the flag restores the user's saved preset (including any `Custom` overrides). `Custom` is not accepted as a value.
247+
248+
**Usage:**
249+
```bash
250+
--graphics high
251+
--graphics medium
252+
--graphics low
253+
```
254+
255+
---
256+
244257
## Development Tools Flags
245258

246259
### `identity-expiration-duration`
@@ -360,6 +373,25 @@ More detailed instructions on how to test can be found in the description of rel
360373

361374
---
362375

376+
## Visual Test Determinism
377+
378+
Visual regression tests need a deterministic scene: a fixed window, no time-of-day drift, no procedural terrain, and no overlapping HUD UI on top of the rendered output. The flags below are the canonical set passed to the Explorer when capturing or comparing reference frames.
379+
380+
| Flag | Effect in visual tests |
381+
| --- | --- |
382+
| `--landscape-terrain-enabled false` | Disables the procedural landscape terrain so the empty/grid background is identical across runs. Requires `--debug` (the flag is gated to debug builds). |
383+
| `--skybox-time-enabled false` | Freezes the skybox time-of-day cycle so lighting, sun position, and shadows stay constant frame-to-frame. |
384+
| `--resolution 1920x1080` | Forces a fixed render resolution. Capturing at the same resolution that the reference frames were taken at avoids upscaler/MSAA differences. Only honored in fullscreen mode. |
385+
| `--windowed-mode` | Forces windowed mode so the OS doesn't apply display-server-specific fullscreen scaling. Pair with `--resolution` to lock the captured framebuffer size. |
386+
| `--disable-hud` | Hides the HUD (chat, minimap, notifications, etc.) so transient UI doesn't pollute the captured frame. SDK UI from scenes remains visible. |
387+
388+
**Example launch:**
389+
```bash
390+
--debug --landscape-terrain-enabled false --skybox-time-enabled false --resolution 1920x1080 --windowed-mode --disable-hud
391+
```
392+
393+
---
394+
363395
## Notes
364396

365397
- Most boolean flags are presence flags (they don't require a value). Simply including `--flag-name` enables the feature.

0 commit comments

Comments
 (0)