diff --git a/Assets/Editor/ExportQualitySettings.cs b/Assets/Editor/ExportQualitySettings.cs
new file mode 100644
index 0000000000..98e69b7316
--- /dev/null
+++ b/Assets/Editor/ExportQualitySettings.cs
@@ -0,0 +1,68 @@
+using UnityEngine;
+using UnityEditor;
+using System.IO;
+using System.Text;
+
+public class ExportQualitySettings
+{
+    [MenuItem("Open Brush/Export Quality Settings to CSV")]
+    private static void ExportQualitySettingsToCSV()
+    {
+        StringBuilder csvContent = new StringBuilder();
+        string filePath = Path.Combine(Application.dataPath, "QualitySettings.csv");
+
+        // Add CSV headers
+        csvContent.AppendLine("Name,pixelLightCount,antiAliasing,realtimeReflectionProbes,resolutionScalingFixedDPIFactor,vSyncCount,anisotropicFiltering,masterTextureLimit,streamingMipmapsActive,streamingMipmapsMemoryBudget,streamingMipmapsRenderersPerFrame,streamingMipmapsMaxLevelReduction,streamingMipmapsMaxFileIORequests,streamingMipmapsAddAllCameras,softParticles,particleRaycastBudget,billboardsFaceCameraPosition,shadowmaskMode,shadows,shadowResolution,shadowProjection,shadowDistance,shadowNearPlaneOffset,shadowCascades,skinWeights,asyncUploadTimeSlice,asyncUploadBufferSize,asyncUploadPersistentBuffer,lodBias,maximumLODLevel,skinWeights");
+
+        for (int i = 0; i < QualitySettings.names.Length; i++)
+        {
+            QualitySettings.SetQualityLevel(i, applyExpensiveChanges: true);
+            string line = string.Format("\"{0}\",{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15},{16},{17},{18},{19},{20},{21},{22},{23},{24},{25},{26},{27},{28},{29},{30}",
+                QualitySettings.names[i],
+                QualitySettings.pixelLightCount,
+                QualitySettings.antiAliasing,
+                QualitySettings.realtimeReflectionProbes,
+                QualitySettings.resolutionScalingFixedDPIFactor,
+                QualitySettings.vSyncCount,
+
+                QualitySettings.anisotropicFiltering,
+                QualitySettings.masterTextureLimit,
+                QualitySettings.streamingMipmapsActive,
+                QualitySettings.streamingMipmapsMemoryBudget,
+                QualitySettings.streamingMipmapsRenderersPerFrame,
+                QualitySettings.streamingMipmapsMaxLevelReduction,
+                QualitySettings.streamingMipmapsMaxFileIORequests,
+                QualitySettings.streamingMipmapsAddAllCameras,
+
+                QualitySettings.softParticles,
+                QualitySettings.particleRaycastBudget,
+
+                QualitySettings.billboardsFaceCameraPosition,
+
+                QualitySettings.shadowmaskMode,
+                QualitySettings.shadows,
+                QualitySettings.shadowResolution,
+                QualitySettings.shadowProjection,
+                QualitySettings.shadowDistance,
+                QualitySettings.shadowNearPlaneOffset,
+                QualitySettings.shadowCascades,
+
+                QualitySettings.skinWeights,
+
+                QualitySettings.asyncUploadTimeSlice,
+                QualitySettings.asyncUploadBufferSize,
+                QualitySettings.asyncUploadPersistentBuffer,
+
+                QualitySettings.lodBias,
+                QualitySettings.maximumLODLevel,
+
+                QualitySettings.skinWeights
+            );
+
+            csvContent.AppendLine(line);
+        }
+
+        File.WriteAllText(filePath, csvContent.ToString());
+        Debug.Log("Quality settings dumped to " + filePath);
+    }
+}
diff --git a/Assets/Editor/ExportQualitySettings.cs.meta b/Assets/Editor/ExportQualitySettings.cs.meta
new file mode 100644
index 0000000000..ccae7c71cd
--- /dev/null
+++ b/Assets/Editor/ExportQualitySettings.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 81b6133882b741a5a8ba17230170238d
+timeCreated: 1709987917
\ No newline at end of file
diff --git a/Assets/Editor/WriteFullUserConfig.cs b/Assets/Editor/WriteFullUserConfig.cs
new file mode 100644
index 0000000000..0ec975f700
--- /dev/null
+++ b/Assets/Editor/WriteFullUserConfig.cs
@@ -0,0 +1,25 @@
+using Newtonsoft.Json;
+using UnityEditor;
+using UnityEngine;
+using System.IO;
+
+namespace TiltBrush
+{
+    public class WriteFullUserConfig
+    {
+        [MenuItem("Open Brush/Write Full User Config")]
+        public static void DoWriteFullUserConfig()
+        {
+            // Quit if we're not in play mode
+            if (!Application.isPlaying)
+            {
+                Debug.LogError("Enter Play Mode and try again.");
+                return;
+            }
+            string path = $"{App.UserPath()}/Full Open Brush.cfg";
+            string json = JsonConvert.SerializeObject(App.UserConfig, Formatting.Indented);
+            File.WriteAllText(path, json);
+            Debug.Log($"User data written to {path}");
+        }
+    }
+}
diff --git a/Assets/Editor/WriteFullUserConfig.cs.meta b/Assets/Editor/WriteFullUserConfig.cs.meta
new file mode 100644
index 0000000000..c1c1a89527
--- /dev/null
+++ b/Assets/Editor/WriteFullUserConfig.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 2e25b0e2c71749bf86fdada7313e666a
+timeCreated: 1709651837
\ No newline at end of file
diff --git a/Assets/QualityLevels Mobile.asset b/Assets/QualityLevels Mobile.asset
index 4141bd01d6..c51ce00580 100644
--- a/Assets/QualityLevels Mobile.asset	
+++ b/Assets/QualityLevels Mobile.asset	
@@ -88,7 +88,7 @@ MonoBehaviour:
     m_MaxSimplificationUserStrokes: 2
     m_GpuLevel: 3
     m_FixedFoveationLevel: 0
-  - m_Bloom: 0
+  - m_Bloom: 3
     m_Hdr: 0
     m_Fxaa: 0
     m_MsaaLevel: 2
@@ -103,7 +103,7 @@ MonoBehaviour:
     m_MaxSimplificationUserStrokes: 2
     m_GpuLevel: 5
     m_FixedFoveationLevel: 0
-  - m_Bloom: 0
+  - m_Bloom: 3
     m_Hdr: 0
     m_Fxaa: 0
     m_MsaaLevel: 2
diff --git a/Assets/Scripts/Brushes/HullBrush.cs b/Assets/Scripts/Brushes/HullBrush.cs
index a1a69a2805..3bc2f22553 100644
--- a/Assets/Scripts/Brushes/HullBrush.cs
+++ b/Assets/Scripts/Brushes/HullBrush.cs
@@ -138,8 +138,8 @@ public HullBrush()
         override public bool ShouldCurrentLineEnd()
         {
             // Reminder: it's ok for this method to be nondeterministic.
-            int localMaxKnotCount = App.PlatformConfig.HullBrushMaxKnots;
-            int maxVertInputCount = App.PlatformConfig.HullBrushMaxVertInputs;
+            int localMaxKnotCount = UserConfig.PerformanceOverrides.HullBrushMaxKnots;
+            int maxVertInputCount = UserConfig.PerformanceOverrides.HullBrushMaxVertInputs;
             int hullInputSize = m_AllVertices.Count(v => !v.DefinitelyInterior);
 
             // For Hull Brush, the limiting factor is the number of points we send to the hull
diff --git a/Assets/Scripts/Export/ExportCollector.cs b/Assets/Scripts/Export/ExportCollector.cs
index 9a9efce3fe..b8a1b2cf25 100644
--- a/Assets/Scripts/Export/ExportCollector.cs
+++ b/Assets/Scripts/Export/ExportCollector.cs
@@ -313,7 +313,7 @@ static ExportUtils.GroupPayload BuildGroupPayload(SceneStatePayload payload,
                         ExportUtils.ReverseTriangleWinding(geometry, 1, 2);
                     }
 
-                    if (App.PlatformConfig.EnableExportMemoryOptimization &&
+                    if (UserConfig.PerformanceOverrides.EnableExportMemoryOptimization &&
                         payload.temporaryDirectory != null)
                     {
                         string filename = Path.Combine(
diff --git a/Assets/Scripts/FileWatcher.cs b/Assets/Scripts/FileWatcher.cs
index 4f3450003b..83e72961c7 100644
--- a/Assets/Scripts/FileWatcher.cs
+++ b/Assets/Scripts/FileWatcher.cs
@@ -27,7 +27,7 @@ public class FileWatcher
 
         public FileWatcher(string path)
         {
-            if (App.PlatformConfig.UseFileSystemWatcher)
+            if (UserConfig.PerformanceOverrides.UseFileSystemWatcher)
             {
                 m_InternalFileWatcher = new FileSystemWatcher(path);
                 AddEventsToInternalFileWatcher();
@@ -36,7 +36,7 @@ public FileWatcher(string path)
 
         public FileWatcher(string path, string filter)
         {
-            if (App.PlatformConfig.UseFileSystemWatcher)
+            if (UserConfig.PerformanceOverrides.UseFileSystemWatcher)
             {
                 m_InternalFileWatcher = new FileSystemWatcher(path, filter);
                 AddEventsToInternalFileWatcher();
diff --git a/Assets/Scripts/GUI/ReferencePanel.cs b/Assets/Scripts/GUI/ReferencePanel.cs
index 7d8abc1ee0..bce3d765ee 100644
--- a/Assets/Scripts/GUI/ReferencePanel.cs
+++ b/Assets/Scripts/GUI/ReferencePanel.cs
@@ -92,7 +92,7 @@ override protected void OnEnablePanel()
                 default:
                     // If we have no filewatcher, we need to check if any files have changed since the user
                     // last had the panel open.
-                    if (!App.PlatformConfig.UseFileSystemWatcher)
+                    if (!UserConfig.PerformanceOverrides.UseFileSystemWatcher)
                     {
                         m_CurrentTab.Catalog.ForceCatalogScan();
                         RefreshPage();
diff --git a/Assets/Scripts/InputManager.cs b/Assets/Scripts/InputManager.cs
index e0b898a0fe..d857c27ad8 100644
--- a/Assets/Scripts/InputManager.cs
+++ b/Assets/Scripts/InputManager.cs
@@ -761,11 +761,6 @@ public bool ControllersAreSwapping()
 
         public Vector2 GetMouseMoveDelta()
         {
-            if (App.Config.IsMobileHardware)
-            {
-                return Vector2.zero;
-            }
-
             Vector2 mv = Mouse.current.delta.ReadValue() * 0.125f;
             return new Vector2(Mathf.Abs(mv.x) > m_InputThreshold ? mv.x : 0f,
                 Mathf.Abs(mv.y) > m_InputThreshold ? mv.y : 0f);
@@ -773,19 +768,14 @@ public Vector2 GetMouseMoveDelta()
 
         public float GetMouseWheel()
         {
-            if (App.Config.IsMobileHardware)
-            {
-                return 0.0f;
-            }
-
-            return Mouse.current.scroll.x.ReadValue();
+            return Mouse.current != null ? Mouse.current.scroll.x.ReadValue() : 0f;
         }
 
         /// Mouse input is ignored on mobile platform because the Oculus Quest seems to emulate mouse
         /// presses when you fiddle with the joystick.
         public bool GetMouseButton(int button)
         {
-            if (App.Config.IsMobileHardware)
+            if (Mouse.current == null)
             {
                 return false;
             }
@@ -805,7 +795,7 @@ public bool GetMouseButton(int button)
         /// presses when you fiddle with the joystick.
         public bool GetMouseButtonDown(int button)
         {
-            if (App.Config.IsMobileHardware)
+            if (Mouse.current == null)
             {
                 return false;
             }
@@ -829,7 +819,7 @@ public bool IsBrushScrollActive()
         public float GetBrushScrollAmount()
         {
             // Check mouse first.
-            if (!App.Config.IsMobileHardware)
+            if (Mouse.current != null)
             {
                 float fMouse = Mouse.current.delta.x.ReadValue();
                 if (Mathf.Abs(fMouse) > m_InputThreshold)
diff --git a/Assets/Scripts/MultiCamCaptureRig.cs b/Assets/Scripts/MultiCamCaptureRig.cs
index 7e6483876b..098b135824 100644
--- a/Assets/Scripts/MultiCamCaptureRig.cs
+++ b/Assets/Scripts/MultiCamCaptureRig.cs
@@ -123,7 +123,7 @@ public void UpdateObjectCameraTransform(MultiCamStyle style, Transform xf, float
 
             // For the mobile version we need to adjust the camera fov so that the frustum exactly matches
             // the view through the viewfinder. The camera is placed at the vr camera position.
-            if (!App.PlatformConfig.EnableMulticamPreview)
+            if (!UserConfig.PerformanceOverrides.EnableMulticamPreview)
             {
                 obj.m_Camera.transform.position = App.VrSdk.GetVrCamera().transform.position;
                 obj.m_Camera.transform.rotation = xf.rotation;
diff --git a/Assets/Scripts/PlatformConfig.cs b/Assets/Scripts/PlatformConfig.cs
index 37a0b583cc..6540844f79 100644
--- a/Assets/Scripts/PlatformConfig.cs
+++ b/Assets/Scripts/PlatformConfig.cs
@@ -86,5 +86,7 @@ public class PlatformConfig : ScriptableObject
         // A mapping between the framerate and the frame gap between multicam previews.
         // (Leave empty to not have any gaps)
         public AnimationCurve FrameRateToPreviewRenderGap;
+
     }
+
 } // namespace TiltBrush
diff --git a/Assets/Scripts/Playback/ScenePlaybackByDistance.cs b/Assets/Scripts/Playback/ScenePlaybackByDistance.cs
index fcd352f721..a181605cae 100644
--- a/Assets/Scripts/Playback/ScenePlaybackByDistance.cs
+++ b/Assets/Scripts/Playback/ScenePlaybackByDistance.cs
@@ -119,7 +119,7 @@ public bool Update()
             // Unity appears to only clean up memory from mucking about with meshes on frame boundaries,
             // So Tilt Brush was using vast amounts of memory to do a quickload.
             // This causes Tilt Brush to only draw a certain distance before returning a frame.
-            float maxDistancePerFrame = App.PlatformConfig.QuickLoadMaxDistancePerFrame;
+            float maxDistancePerFrame = UserConfig.PerformanceOverrides.QuickLoadMaxDistancePerFrame;
             if (m_metersRemaining > maxDistancePerFrame)
             {
                 m_metersRemaining = maxDistancePerFrame;
diff --git a/Assets/Scripts/ReferenceImage.cs b/Assets/Scripts/ReferenceImage.cs
index 251e3bdf5c..3b82813897 100644
--- a/Assets/Scripts/ReferenceImage.cs
+++ b/Assets/Scripts/ReferenceImage.cs
@@ -202,7 +202,7 @@ IEnumerable LoadImage(string path, Texture2D dest, bool runForeground = false)
             // ReturnImageFullSize is called during load.
             m_FullSizeReferences++;
             var reader = new ThreadedImageReader(path, -1,
-                App.PlatformConfig.ReferenceImagesMaxDimension);
+                UserConfig.PerformanceOverrides.ReferenceImagesMaxDimension);
             while (!reader.Finished)
             {
                 if (!runForeground) { yield return null; }
@@ -214,7 +214,7 @@ IEnumerable LoadImage(string path, Texture2D dest, bool runForeground = false)
                 result = reader.Result;
                 if (result != null && dest != null)
                 {
-                    int resizeLimit = App.PlatformConfig.ReferenceImagesResizeDimension;
+                    int resizeLimit = UserConfig.PerformanceOverrides.ReferenceImagesResizeDimension;
                     if (result.ColorWidth > resizeLimit || result.ColorHeight > resizeLimit)
                     {
                         // Resize the image to the resize limit before saving it to the dest texture.
@@ -354,7 +354,7 @@ public bool RequestLoad(bool allowMainThread = false)
 
                 ImageCache.SaveImageCache(tex, FilePath);
                 m_ImageAspect = (float)tex.width / tex.height;
-                int resizeLimit = App.PlatformConfig.ReferenceImagesResizeDimension;
+                int resizeLimit = UserConfig.PerformanceOverrides.ReferenceImagesResizeDimension;
                 if (tex.width > resizeLimit || tex.height > resizeLimit)
                 {
                     Texture2D resizedTex = new Texture2D(2, 2, TextureFormat.RGBA32, true);
@@ -388,7 +388,7 @@ public bool RequestLoad(bool allowMainThread = false)
 
                 ImageCache.SaveImageCache(tex, FilePath);
                 m_ImageAspect = (float)tex.width / tex.height;
-                int resizeLimit = App.PlatformConfig.ReferenceImagesResizeDimension;
+                int resizeLimit = UserConfig.PerformanceOverrides.ReferenceImagesResizeDimension;
                 if (tex.width > resizeLimit || tex.height > resizeLimit)
                 {
                     Texture2D resizedTex = new Texture2D(2, 2, TextureFormat.RGBA32, true);
@@ -526,7 +526,7 @@ IEnumerator<Timeslice> RequestLoadCoroutineMainThread()
                     yield return null;
 
                     // Create the full size image cache as well.
-                    int resizeLimit = App.PlatformConfig.ReferenceImagesResizeDimension;
+                    int resizeLimit = UserConfig.PerformanceOverrides.ReferenceImagesResizeDimension;
                     if (inTex.width > resizeLimit || inTex.height > resizeLimit)
                     {
                         Texture2D resizedTex = new Texture2D(2, 2, TextureFormat.RGBA32, true);
@@ -555,7 +555,7 @@ IEnumerable<Timeslice> RequestLoadCoroutine()
         {
             var reader = new ThreadedImageReader(m_Path,
                 ReferenceImageCatalog.MAX_ICON_TEX_DIMENSION,
-                App.PlatformConfig.ReferenceImagesMaxDimension);
+                UserConfig.PerformanceOverrides.ReferenceImagesMaxDimension);
             while (!reader.Finished)
             {
                 yield return null;
diff --git a/Assets/Scripts/ReferencePanelTab.cs b/Assets/Scripts/ReferencePanelTab.cs
index a3b61c3c04..2d5c820c13 100644
--- a/Assets/Scripts/ReferencePanelTab.cs
+++ b/Assets/Scripts/ReferencePanelTab.cs
@@ -104,7 +104,7 @@ public virtual void UpdateTab() { }
 
         public virtual void OnTabEnable()
         {
-            if (!App.PlatformConfig.UseFileSystemWatcher)
+            if (!UserConfig.PerformanceOverrides.UseFileSystemWatcher)
             {
                 Catalog.ForceCatalogScan();
             }
diff --git a/Assets/Scripts/Save/SaveLoadScript.cs b/Assets/Scripts/Save/SaveLoadScript.cs
index 9b490d66d5..ad7e7dbc20 100644
--- a/Assets/Scripts/Save/SaveLoadScript.cs
+++ b/Assets/Scripts/Save/SaveLoadScript.cs
@@ -156,7 +156,7 @@ public bool AutosaveEnabled
             get
             {
                 return !m_AutosaveFailed &&
-                    App.PlatformConfig.EnableAutosave &&
+                    UserConfig.PerformanceOverrides.EnableAutosave &&
                     !App.UserConfig.Flags.DisableAutosave;
             }
         }
diff --git a/Assets/Scripts/ScreenshotManager.cs b/Assets/Scripts/ScreenshotManager.cs
index f887a9ccbf..4b70c03c04 100644
--- a/Assets/Scripts/ScreenshotManager.cs
+++ b/Assets/Scripts/ScreenshotManager.cs
@@ -180,7 +180,7 @@ void Start()
                 SceneSettings.m_Instance.RegisterCamera(m_RightInfo.camera);
             }
 
-            if (!App.Config.PlatformConfig.EnableMulticamPreview)
+            if (!UserConfig.PerformanceOverrides.EnableMulticamPreview)
             {
                 // If we're looking through the viewfinder, we need to make some changes to this camera
                 SetScreenshotResolution(App.UserConfig.Flags.SnapshotWidth > 0
diff --git a/Assets/Scripts/Sharing/DriveSync.cs b/Assets/Scripts/Sharing/DriveSync.cs
index ad8c68ed2e..b7100a4ce8 100644
--- a/Assets/Scripts/Sharing/DriveSync.cs
+++ b/Assets/Scripts/Sharing/DriveSync.cs
@@ -463,17 +463,14 @@ private async Task SetupSyncFoldersAsync(CancellationToken token)
                     SyncType.Upload,
                     SyncedFolderType.MediaLibrary,
                     token));
-                if (!App.Config.IsMobileHardware)
-                {
-                    folderSyncs.Add(AddSyncedFolderAsync(
-                        "Models",
-                        App.ModelLibraryPath(),
-                        mediaLibrary.Id,
-                        SyncType.Upload,
-                        SyncedFolderType.MediaLibrary,
-                        token,
-                        recursive: true));
-                }
+                folderSyncs.Add(AddSyncedFolderAsync(
+                    "Models",
+                    App.ModelLibraryPath(),
+                    mediaLibrary.Id,
+                    SyncType.Upload,
+                    SyncedFolderType.MediaLibrary,
+                    token,
+                    recursive: true));
                 folderSyncs.Add(AddSyncedFolderAsync(
                     "Videos",
                     App.VideoLibraryPath(),
@@ -493,26 +490,23 @@ private async Task SetupSyncFoldersAsync(CancellationToken token)
                     token));
             }
 
-            if (!App.Config.IsMobileHardware)
+            if (IsFolderOfTypeSynced(SyncedFolderType.Videos))
             {
-                if (IsFolderOfTypeSynced(SyncedFolderType.Videos))
-                {
-                    folderSyncs.Add(AddSyncedFolderAsync(
-                        "Videos",
-                        App.VideosPath(),
-                        deviceRootId,
-                        SyncType.Upload,
-                        SyncedFolderType.Videos,
-                        token,
-                        excludeExtensions: new[] { ".bat", ".usda" }));
-                    folderSyncs.Add(AddSyncedFolderAsync(
-                        "VrVideos",
-                        App.VrVideosPath(),
-                        deviceRootId,
-                        SyncType.Upload,
-                        SyncedFolderType.Videos,
-                        token));
-                }
+                folderSyncs.Add(AddSyncedFolderAsync(
+                    "Videos",
+                    App.VideosPath(),
+                    deviceRootId,
+                    SyncType.Upload,
+                    SyncedFolderType.Videos,
+                    token,
+                    excludeExtensions: new[] { ".bat", ".usda" }));
+                folderSyncs.Add(AddSyncedFolderAsync(
+                    "VrVideos",
+                    App.VrVideosPath(),
+                    deviceRootId,
+                    SyncType.Upload,
+                    SyncedFolderType.Videos,
+                    token));
             }
 
             if (IsFolderOfTypeSynced(SyncedFolderType.Exports))
diff --git a/Assets/Scripts/Sharing/OAuth2Identity.cs b/Assets/Scripts/Sharing/OAuth2Identity.cs
index a9e66905c9..555d28f099 100644
--- a/Assets/Scripts/Sharing/OAuth2Identity.cs
+++ b/Assets/Scripts/Sharing/OAuth2Identity.cs
@@ -150,6 +150,7 @@ public async Task InitializeAsync()
                 return;
             }
             m_TokenDataStore = new PlayerPrefsDataStore(m_TokenStorePrefix);
+            // m_AdditionalDesktopOAuthScopes is currently not used
             var scopes = App.Config.IsMobileHardware
                 ? m_OAuthScopes
                 : m_OAuthScopes.Concat(m_AdditionalDesktopOAuthScopes).ToArray();
diff --git a/Assets/Scripts/Sharing/WebRequest.cs b/Assets/Scripts/Sharing/WebRequest.cs
index 54e116a297..c535c39d9d 100644
--- a/Assets/Scripts/Sharing/WebRequest.cs
+++ b/Assets/Scripts/Sharing/WebRequest.cs
@@ -352,7 +352,7 @@ public async Task<Reply> SendNamedDataAsync(
                 RequestDebugLogFile(debugId, "form", temporaryFileName);
 
                 Func<UploadHandler> payloadCreator;
-                if (App.PlatformConfig.AvoidUploadHandlerFile)
+                if (UserConfig.PerformanceOverrides.AvoidUploadHandlerFile)
                 {
                     byte[] fileContents = await Task.Run(() => File.ReadAllBytes(temporaryFileName));
                     payloadCreator = () => new UploadHandlerRaw(fileContents);
diff --git a/Assets/Scripts/SketchMemoryScript.cs b/Assets/Scripts/SketchMemoryScript.cs
index 7b7420ec5e..08dd64c2d8 100644
--- a/Assets/Scripts/SketchMemoryScript.cs
+++ b/Assets/Scripts/SketchMemoryScript.cs
@@ -317,7 +317,7 @@ void Awake()
             m_Instance = this;
             m_xfSketchInitial_RS = TrTransform.identity;
 
-            m_MemoryWarningVertCount = App.PlatformConfig.MemoryWarningVertCount;
+            m_MemoryWarningVertCount = UserConfig.PerformanceOverrides.MemoryWarningVertCount;
         }
 
         void Update()
diff --git a/Assets/Scripts/Tools/MultiCamTool.cs b/Assets/Scripts/Tools/MultiCamTool.cs
index ab6053b0c8..38da3c6ac4 100644
--- a/Assets/Scripts/Tools/MultiCamTool.cs
+++ b/Assets/Scripts/Tools/MultiCamTool.cs
@@ -362,7 +362,7 @@ override public void Init()
 
             // If no viewfinder preview is shown, then we need to adjust the time between shots to allow
             // for the flash animation.
-            if (!App.PlatformConfig.EnableMulticamPreview)
+            if (!UserConfig.PerformanceOverrides.EnableMulticamPreview)
             {
                 m_MinTimeBetweenShots =
                     SketchControlsScript.m_Instance.MultiCamCaptureRig.SnapshotFlashDuration;
@@ -631,7 +631,7 @@ void UpdateMultiCamTransform()
                 transform.rotation = InputManager.Brush.Geometry.CameraAttachPoint.rotation;
 
                 // Does the viewfinder need to face the user?
-                if (!App.PlatformConfig.EnableMulticamPreview)
+                if (!UserConfig.PerformanceOverrides.EnableMulticamPreview)
                 {
                     var camXform = App.VrSdk.GetVrCamera().transform;
                     // Calculate the up and forward vectors so that the up is taken from the orientation of the
@@ -1398,8 +1398,8 @@ void TurnOn()
 
             m_SwipeHintCountdown = m_SwipeHintDelay;
             m_CurrentState = State.Enter;
-            SketchControlsScript.m_Instance.MultiCamCaptureRig.EnableScreen(App.PlatformConfig.EnableMulticamPreview);
-            SketchControlsScript.m_Instance.MultiCamCaptureRig.EnableCamera(App.PlatformConfig.EnableMulticamPreview);
+            SketchControlsScript.m_Instance.MultiCamCaptureRig.EnableScreen(UserConfig.PerformanceOverrides.EnableMulticamPreview);
+            SketchControlsScript.m_Instance.MultiCamCaptureRig.EnableCamera(UserConfig.PerformanceOverrides.EnableMulticamPreview);
         }
 
         override public void AssignControllerMaterials(InputManager.ControllerName controller)
@@ -1806,7 +1806,7 @@ IEnumerator TakeScreenshotAsync(string saveName)
 
             AudioManager.m_Instance.PlayScreenshotSound(transform.position);
 
-            if (!App.Config.PlatformConfig.EnableMulticamPreview)
+            if (!UserConfig.PerformanceOverrides.EnableMulticamPreview)
             {
                 SketchControlsScript.m_Instance.MultiCamCaptureRig.EnableCamera(true);
                 yield return null;
@@ -1842,7 +1842,7 @@ IEnumerator TakeScreenshotAsync(string saveName)
                     rMgr.RenderToTexture(tmp);
                     wrapper.SuperSampling = ssaaRestore;
                     yield return null;
-                    SketchControlsScript.m_Instance.MultiCamCaptureRig.EnableCamera(App.PlatformConfig.EnableMulticamPreview);
+                    SketchControlsScript.m_Instance.MultiCamCaptureRig.EnableCamera(UserConfig.PerformanceOverrides.EnableMulticamPreview);
 
                     string fullPath = Path.GetFullPath(saveName);
                     System.Object err = null;
@@ -1867,7 +1867,7 @@ IEnumerator TakeScreenshotAsync(string saveName)
                     OutputWindowScript.ReportFileSaved("Snapshot Saved!", saveName,
                         OutputWindowScript.InfoCardSpawnPos.Brush);
 
-                    if (!App.PlatformConfig.EnableMulticamPreview)
+                    if (!UserConfig.PerformanceOverrides.EnableMulticamPreview)
                     {
                         var multiCam = SketchControlsScript.m_Instance.MultiCamCaptureRig;
                         yield return multiCam.SnapshotFlashAnimation(m_CurrentCameraIndex, tmp);
diff --git a/Assets/Scripts/UserConfig.cs b/Assets/Scripts/UserConfig.cs
index 1a811652b8..a8d7487a95 100644
--- a/Assets/Scripts/UserConfig.cs
+++ b/Assets/Scripts/UserConfig.cs
@@ -183,6 +183,65 @@ public bool PolyModelPreload
 
         public FlagsConfig Flags;
 
+        [Serializable]
+        public struct PerformanceConfig
+        {
+            // Comments show defaults for Mobile / PC
+            public int? HullBrushMaxVertInputs;          // 2500 / 400
+            public int? HullBrushMaxKnots;               // 6000 / 900
+            public int? ReferenceImagesMaxFileSize;      // 2147483647 / 10485760
+            public int? ReferenceImagesMaxDimension;     // 2147483647 / 4352
+            public int? ReferenceImagesResizeDimension;  // 2147483647 / 1024
+            public int? MemoryWarningVertCount;          // 2147483647 / 1000000
+            public bool? UseFileSystemWatcher;           // true / false
+            public bool? EnableAutosave;                 // true / false
+            public float? QuickLoadMaxDistancePerFrame;  // 40 / 4
+            public bool? AvoidUploadHandlerFile;         // true / false
+            public bool? EnableExportMemoryOptimization; // true / true
+            public bool? EnableMulticamPreview;          // true / true
+            public int? MaxSnapshotDimension;            // 16000 / 4096
+
+
+            public bool? QuestDynamicFoveation;
+            public bool? QuestDynamicResolution;
+            public int? OverrideQuestGPULevel;
+            public int? OverrideQuestFoveationLevel;
+
+            public int? OverrideQualityLevel;
+
+            public bool? AnisotropicFiltering;         // true / false
+            public bool? BillboardsFaceCameraPosition; // true / false
+            public int? ShadowMode;                    // HardOnly / Disable
+            public int? ShadowResolution;              // High / Low
+            public int? ShadowDistance;                // 30 / 15
+            public int? LodBias;                       // 1 / 0.3
+            public int? SkinWeights;                   // 2 / 1
+
+
+            public int? OverrideBloomMode;
+        }
+
+        public PerformanceConfig Performance;
+
+        public static class PerformanceOverrides
+        {
+            // User overrides
+            private static PerformanceConfig o = App.UserConfig.Performance;
+            public static int HullBrushMaxVertInputs => o.HullBrushMaxVertInputs ?? App.PlatformConfig.HullBrushMaxVertInputs;
+            public static int HullBrushMaxKnots => o.HullBrushMaxKnots ?? App.PlatformConfig.HullBrushMaxKnots;
+            public static float ReferenceImagesMaxFileSize => o.ReferenceImagesMaxFileSize ?? App.PlatformConfig.ReferenceImagesMaxFileSize;
+            public static int ReferenceImagesMaxDimension => o.ReferenceImagesMaxDimension ?? App.PlatformConfig.ReferenceImagesMaxDimension;
+            public static int ReferenceImagesResizeDimension => o.ReferenceImagesResizeDimension ?? App.PlatformConfig.ReferenceImagesResizeDimension;
+            public static int MemoryWarningVertCount => o.MemoryWarningVertCount ?? App.PlatformConfig.MemoryWarningVertCount;
+            public static bool UseFileSystemWatcher => o.UseFileSystemWatcher ?? App.PlatformConfig.UseFileSystemWatcher;
+            public static bool EnableAutosave => o.EnableAutosave ?? App.PlatformConfig.EnableAutosave;
+            public static float QuickLoadMaxDistancePerFrame => o.QuickLoadMaxDistancePerFrame ?? App.PlatformConfig.QuickLoadMaxDistancePerFrame;
+            public static bool AvoidUploadHandlerFile => o.AvoidUploadHandlerFile ?? App.PlatformConfig.AvoidUploadHandlerFile;
+            public static bool EnableExportMemoryOptimization => o.EnableExportMemoryOptimization ?? App.PlatformConfig.EnableExportMemoryOptimization;
+            public static bool EnableMulticamPreview => o.EnableMulticamPreview ?? App.PlatformConfig.EnableMulticamPreview;
+            public static float MaxSnapshotDimension => o.MaxSnapshotDimension ?? App.PlatformConfig.MaxSnapshotDimension;
+        }
+
         [Serializable]
         public struct DemoConfig
         {