Skip to content
This repository has been archived by the owner on May 11, 2024. It is now read-only.

Commit

Permalink
Multithread rdb icon loading
Browse files Browse the repository at this point in the history
  • Loading branch information
Dual-Iron committed May 3, 2022
1 parent f96ef0a commit 7838bb7
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 76 deletions.
1 change: 0 additions & 1 deletion Realm/EntryPoint.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Logging;
using BepInEx.Preloader.Patching;
using Mono.Cecil;
using Mono.Cecil.Cil;
using MonoMod.Cil;
Expand Down
66 changes: 66 additions & 0 deletions Realm/Gui/AsyncIcon.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Realm.Assets;
using UnityEngine;
using Realm.Jobs;

namespace Realm.Gui;

public enum AsyncIconStatus { Unstarted, Loading, Loaded, Errored }

sealed class AsyncIcon
{
private readonly string iconFilename;
private readonly string iconURL;

public AsyncIconStatus Status { get; private set; }

public AsyncIcon(string iconFilename, string iconURL)
{
this.iconFilename = iconFilename;
this.iconURL = iconURL;
}

public void StartLoading()
{
Status = AsyncIconStatus.Loading;
Job.Start(() => Status = Load());
}

private AsyncIconStatus Load()
{
if (Futile.atlasManager._allElementsByName.ContainsKey(iconURL)) {
return AsyncIconStatus.Loaded;
}

string path = Path.Combine(RealmPaths.IconFolder.FullName, iconFilename);

if (File.Exists(path) && (DateTime.UtcNow - File.GetLastWriteTimeUtc(path)).TotalDays < 10) {
return GetIcon(path);
}

var proc = BackendProcess.Execute($"-dl \"{iconURL}\" \"{path}\"");
if (proc.ExitCode == 0 && File.Exists(path)) {
return GetIcon(path);
}

Program.Logger.LogError($"Error downloading icon for {iconFilename}. {proc}");

return AsyncIconStatus.Errored;
}

private AsyncIconStatus GetIcon(string iconPath)
{
using FileStream stream = File.OpenRead(iconPath);

Texture2D tex = Asset.LoadTexture(stream);

if (tex.width != 128 || tex.height != 128) {
Program.Logger.LogError($"Icon texture for {iconFilename} was not 128x128");
UnityEngine.Object.Destroy(tex);
return AsyncIconStatus.Errored;
}
else {
HeavyTexturesCache.LoadAndCacheAtlasFromTexture(iconURL, tex);
return AsyncIconStatus.Loaded;
}
}
}
26 changes: 26 additions & 0 deletions Realm/Gui/Elements/LoadSpinny.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Menu;
using Realm.Assets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

namespace Realm.Gui.Elements;

sealed class LoadSpinny : PositionedMenuObject
{
public readonly MenuSprite ico;

public LoadSpinny(MenuObject owner, Vector2 pos) : base(owner.menu, owner, pos)
{
subObjects.Add(ico = new(this, default, Asset.SpriteFromRes("HARDHAT")));
}

public override void Update()
{
base.Update();

ico.sprite.rotation += 360f / 40f;
}
}
1 change: 0 additions & 1 deletion Realm/Gui/Gui.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Menu;
using Realm.Gui.Menus;
using UnityEngine;

namespace Realm.Gui;

Expand Down
8 changes: 3 additions & 5 deletions Realm/Gui/Menus/AudbPane.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
using Menu;
using static Menu.Menu;
using Menu;
using Realm.Assets;
using UnityEngine;
using static Menu.Menu;
using Realm.Jobs;
using System.Diagnostics;
using Realm.ModLoading;
using System.Threading;
using Realm.Gui.Elements;
using Rwml;

Expand Down Expand Up @@ -95,7 +93,7 @@ enum DownloadStatus { None, InProgress, Success, Err }

public override void Update()
{
downloadBtn.buttonBehav.greyedOut = BlockInteraction || availability == Availability.Installed || downloadStatus is DownloadStatus.InProgress;
downloadBtn.buttonBehav.greyedOut = BlockInteraction || availability == Availability.Installed || downloadStatus == DownloadStatus.InProgress;

// If the download just finished, display its message
if (downloadMessage != null) {
Expand Down
8 changes: 3 additions & 5 deletions Realm/Gui/Menus/Browser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ sealed class Browser : ModMenuPage
- RDB mods (the ones with icons), and
- AUDB mods (the ones without icons).";

readonly MenuSprite loadSpinner;
readonly LoadSpinny loadSpinny;
readonly Listing rdbListing;
readonly MenuLabel warningLabel;
readonly TextBox search;
Expand Down Expand Up @@ -52,8 +52,7 @@ public Browser(MenuObject owner, Vector2 pos) : base(owner, pos)
OnInsert = UpdateSearch
}));

subObjects.Add(loadSpinner = new(this, rdbListing.pos + rdbListing.size + new Vector2(-24, 8+12), Asset.SpriteFromRes("HARDHAT")));

subObjects.Add(loadSpinny = new(this, rdbListing.pos + rdbListing.size + new Vector2(-24, 8+12)));
subObjects.Add(warningLabel = new(menu, this, "", rdbListing.pos, rdbListing.size, true));

pageState.LoadPage();
Expand All @@ -65,8 +64,7 @@ public override void Update()

search.GetButtonBehavior.greyedOut = pageState.State == Errored || BlockMenuInteraction;

loadSpinner.sprite.rotation += 360f / 40f;
loadSpinner.sprite.isVisible = pageState.State == LoadingPages;
loadSpinny.ico.sprite.isVisible = pageState.State == LoadingPages;

if (pageState.State == Errored) {
if (warningLabel.text == "") {
Expand Down
88 changes: 28 additions & 60 deletions Realm/Gui/Menus/BrowserPane.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ sealed class BrowserPane : RectangularMenuObject, IListable, IHoverable
subObjects.Add(inner);

inner.subObjects.Add(icon = new MenuSprite(inner, default, Asset.SpriteFromRes("NO_ICON")));
icon.sprite.isVisible = false;

SetIcon();
inner.subObjects.Add(iconSpinny = new(inner, new Vector2(52, 52)));

iconLoader = new($"{entry.Owner}~{entry.Name}", entry.Icon);
iconLoader.StartLoading();

// Add name + owner
Label(entry.Name.CullLong("DisplayFont", Width - 128 - verWidth - pad * 2), pos: new(128 + pad, 110), true).WithAlignment(FLabelAlignment.Left);
Expand Down Expand Up @@ -82,66 +86,24 @@ MenuLabel Label(string txt, Vector2 pos, bool big = false)
}
}

int loadIcon; // 0 for loading, 1 for loaded and ready to finish, 2 for finished

private void SetIcon()
{
if (Futile.atlasManager._allElementsByName.ContainsKey(entry.Icon)) {
Interlocked.Exchange(ref loadIcon, 1);
return;
}

string path = Path.Combine(RealmPaths.IconFolder.FullName, $"{entry.Owner}~{entry.Name}");

if (File.Exists(path) && (DateTime.UtcNow - File.GetLastWriteTimeUtc(path)).TotalDays < 10) {
LoadIcon();
return;
}

BackendProcess proc = BackendProcess.Execute($"-dl \"{entry.Icon}\" \"{path}\"");

if (proc.ExitCode != 0) {
Program.Logger.LogError($"Error downloading icon for {entry.Owner}/{entry.Name}. {proc}");
}
else if (File.Exists(path)) {
LoadIcon();
}

void LoadIcon()
{
using FileStream stream = File.OpenRead(path);
Texture2D tex = Asset.LoadTexture(stream);

if (tex.width != 128 || tex.height != 128) {
Program.Logger.LogError($"Icon texture for {entry.Owner}/{entry.Name} was not 128x128");
UnityEngine.Object.Destroy(tex);
}
else {
HeavyTexturesCache.LoadAndCacheAtlasFromTexture(entry.Icon, tex);
Interlocked.Exchange(ref loadIcon, 1);
}
}
}

enum Availability { CanInstall, Installed, CanUpdate }
enum DownloadStatus { None, InProgress, Success, Err }

public readonly RdbEntry entry;

readonly AsyncIcon iconLoader;
readonly LoadSpinny iconSpinny;
readonly MenuSprite icon;
readonly SymbolButton downloadBtn;
readonly SymbolButton homepageBtn;
readonly MultiLabel status;
readonly Availability availability;

const int DownloadInProgress = 1;
const int DownloadSuccess = 2;
const int DownloadErr = 3;

int downloadStatus; // must be int for use with Interlocked.Exchange (see Download() method)
Availability availability;
DownloadStatus downloadStatus;
string? downloadMessage;
bool previewingHomepage;

public bool PreventButtonClicks => downloadStatus == DownloadInProgress;
public bool PreventButtonClicks => downloadStatus == DownloadStatus.InProgress;

public bool IsBelow { get; set; }
public bool BlockInteraction { get; set; }
Expand All @@ -151,13 +113,13 @@ enum Availability { CanInstall, Installed, CanUpdate }

public override void Update()
{
downloadBtn.buttonBehav.greyedOut = BlockInteraction || availability == Availability.Installed || downloadStatus is DownloadInProgress or DownloadSuccess;
downloadBtn.buttonBehav.greyedOut = BlockInteraction || availability == Availability.Installed || downloadStatus == DownloadStatus.InProgress;
homepageBtn.buttonBehav.greyedOut = BlockInteraction || entry.Homepage.Trim().Length == 0;

// If the download just finished, display its message
if (downloadMessage != null) {
string culledMessage = downloadMessage.Replace('\n', ' ').CullLong("font", Width - status.pos.x - 8);
Color color = downloadStatus == DownloadSuccess ? new(.5f, 1, .5f) : new(1, .5f, .5f);
Color color = downloadStatus == DownloadStatus.Success ? new(.5f, 1, .5f) : new(1, .5f, .5f);
status.SetLabel(color, culledMessage);

previewingHomepage = false;
Expand All @@ -179,8 +141,13 @@ public override void GrafUpdate(float timeStacker)

Container.alpha = (float)Mathf.Pow(Mathf.InverseLerp(0.5f, 1, Visibility), 3);

if (loadIcon == 1) {
loadIcon = 2;
if (iconLoader.Status == AsyncIconStatus.Errored) {
iconSpinny.ico.sprite.isVisible = false;
icon.sprite.isVisible = true;
}
else if (iconLoader.Status == AsyncIconStatus.Loaded && !icon.sprite.isVisible) {
iconSpinny.ico.sprite.isVisible = false;
icon.sprite.isVisible = true;
icon.sprite.element = Futile.atlasManager._allElementsByName[entry.Icon];
}
}
Expand All @@ -191,7 +158,7 @@ public override void Singal(MenuObject sender, string message)
{
if (sender == downloadBtn) {
previewingHomepage = false;
downloadStatus = DownloadInProgress;
downloadStatus = DownloadStatus.InProgress;

status.SetLabel(MenuRGB(MenuColors.MediumGrey), "Downloading");
menu.PlaySound(SoundID.MENU_Button_Standard_Button_Pressed);
Expand Down Expand Up @@ -226,22 +193,23 @@ private void Download()
if (proc.ExitCode == 0) {
Program.Logger.LogInfo($"Downloaded {entry.Owner}/{entry.Name}");

Interlocked.Exchange(ref downloadStatus, DownloadSuccess);
Interlocked.Exchange(ref downloadMessage, "Download successful");
availability = Availability.Installed;
downloadStatus = DownloadStatus.Success;
downloadMessage = "Download successful";

if (menu is ModMenu m) {
m.NeedsRefresh = true;
}
}
else if (proc.ExitCode == null) {
Interlocked.Exchange(ref downloadStatus, DownloadErr);
Interlocked.Exchange(ref downloadMessage, "Download timed out");
downloadStatus = DownloadStatus.Err;
downloadMessage = "Download timed out";
}
else {
Program.Logger.LogError($"Failed to download {entry.Owner}/{entry.Name} with err {proc.ExitCode}.\n{proc.Error}");

Interlocked.Exchange(ref downloadStatus, DownloadErr);
Interlocked.Exchange(ref downloadMessage, proc.Error);
downloadStatus = DownloadStatus.Err;
downloadMessage = proc.Error;
}
}

Expand Down
4 changes: 1 addition & 3 deletions Realm/ModLoading/AudbEntry.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using UnityEngine;

namespace Realm.ModLoading;
namespace Realm.ModLoading;

sealed class AudbEntry
{
Expand Down
2 changes: 1 addition & 1 deletion global.targets
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>0.10.0</Version>
<Version>0.10.1</Version>
<LangVersion>preview</LangVersion>
</PropertyGroup>
</Project>

0 comments on commit 7838bb7

Please sign in to comment.