Repository navigation
Build: prepare the shaders at the end of the build, not at the first start #1026
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
8df61e6
126449c
c6deacd
d6594bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| namespace HookshotBuilder; | ||
|
|
||
| /// What the player chose last time (the install folder, the game files, the shaders' tick): builder-settings.json beside the versions | ||
| sealed class BuilderSettings | ||
| { | ||
| public string? Out { get; set; } | ||
| public string? Nup { get; set; } | ||
| public string? Romfs { get; set; } | ||
| public string? Exefs { get; set; } | ||
| public bool Dlss { get; set; } // (NVIDIA DLSS ticked: NVIDIA's license was accepted) | ||
| public bool PrepareLater { get; set; } // ("Prepare shaders now" unticked) | ||
|
|
||
| public static BuilderSettings? Load(string path) | ||
| { | ||
| try { return System.Text.Json.JsonSerializer.Deserialize<BuilderSettings>(System.IO.File.ReadAllText(path)); } | ||
| catch (Exception) { return null; } | ||
| } | ||
|
|
||
| /// (the updates, from the launcher's Update and from the --update start after a self-update, begin before ApplySettings sets | ||
| /// the tick from these settings: they read the saved choice itself. None saved: prepared, as the tick's default) | ||
| public static bool PrepareNow(string path) => Load(path)?.PrepareLater != true; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -252,23 +252,15 @@ void LaunchPlayClick(object sender, RoutedEventArgs e) | |
| void LaunchFolderClick(object sender, RoutedEventArgs e) => | ||
| Process.Start(new ProcessStartInfo(launchFolder) { UseShellExecute = true }); | ||
|
|
||
| // ---- what the player chose last time (the install folder, the game files): builder-settings.json beside the versions ---- | ||
| sealed class BuilderSettings | ||
| { | ||
| public string? Out { get; set; } | ||
| public string? Nup { get; set; } | ||
| public string? Romfs { get; set; } | ||
| public string? Exefs { get; set; } | ||
| public bool Dlss { get; set; } // (NVIDIA DLSS ticked: NVIDIA's license was accepted) | ||
| } | ||
| // ---- what the player chose last time (BuilderSettings.cs): builder-settings.json beside the versions ---- | ||
| static string SettingsPath => System.IO.Path.Combine( | ||
| Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "HookshotBuilder", "builder-settings.json"); | ||
|
|
||
| void ApplySettings() | ||
| { | ||
| BuilderSettings? s = null; | ||
| try { s = System.Text.Json.JsonSerializer.Deserialize<BuilderSettings>(File.ReadAllText(SettingsPath)); } catch (Exception) { } | ||
| var s = BuilderSettings.Load(SettingsPath); | ||
| if (s == null) return; | ||
| PrepareTick.IsChecked = !s.PrepareLater; | ||
| if (!string.IsNullOrEmpty(s.Out)) OutPath.Text = s.Out; | ||
| DlssTick.IsChecked = s.Dlss; | ||
| if (s.Romfs != null && s.Exefs != null && Directory.Exists(s.Romfs) && File.Exists(System.IO.Path.Combine(s.Exefs, "main"))) | ||
|
|
@@ -293,7 +285,8 @@ void SaveSettings() | |
| try | ||
| { | ||
| var s = new BuilderSettings { Out = OutPath.Text, Nup = romfs == null ? chosenNup : null, Romfs = romfs, Exefs = exefs, | ||
| Dlss = DlssTick.IsChecked == true }; | ||
| Dlss = DlssTick.IsChecked == true, | ||
| PrepareLater = PrepareTick.IsChecked != true }; | ||
| Directory.CreateDirectory(System.IO.Path.GetDirectoryName(SettingsPath)!); | ||
| File.WriteAllText(SettingsPath, System.Text.Json.JsonSerializer.Serialize(s)); | ||
| } | ||
|
|
@@ -306,7 +299,7 @@ void Preview(string what) | |
| switch (what) | ||
| { | ||
| case "build": | ||
| MakeRows(update: false); | ||
| MakeRows(update: false, shaders: true); | ||
| SetStep(1); | ||
| ShowPage(BuildPage); | ||
| rows["read"].Begin(); rows["read"].Done(); rows["read"].Small("Version checked"); | ||
|
|
@@ -692,7 +685,8 @@ async void StartBuild(object sender, RoutedEventArgs e) | |
| else if (chosenNup != null) args.AddRange(new[] { "--nup", chosenNup }); | ||
| else return; | ||
| args.AddRange(new[] { "--out", OutPath.Text, "--language", lang.Code }); | ||
| Run(tools, args, dlss: DlssTick.IsChecked == true); | ||
| prepareNow = PrepareTick.IsChecked == true; | ||
| Run(tools, ShaderStep.Args(args, prepareNow), dlss: DlssTick.IsChecked == true); | ||
| } | ||
|
|
||
| const string DlssLicense = | ||
|
|
@@ -719,17 +713,30 @@ static readonly (string Id, string Build, string Update)[] Stages = | |
| ("lift", "Translate the game to PC code", "Translate the game to PC code"), | ||
| ("compile", "Compile", "Compile changed files"), | ||
| ("finish", "Put the game together", "Swap in the new game"), | ||
| (ShaderStep.Id, "Prepare the shaders", "Prepare the shaders"), | ||
| }; | ||
| static readonly Dictionary<string, (double From, double Size)> Weight = new() | ||
| static readonly Dictionary<string, (double From, double Size)> WithShaders = new() | ||
| { | ||
| ["download"] = (0, 0.04), ["read"] = (0.04, 0.04), ["lift"] = (0.08, 0.14), ["compile"] = (0.22, 0.68), ["finish"] = (0.90, 0.02), | ||
| [ShaderStep.Id] = (0.92, 0.08), // (hookshot --prepare-shaders: minutes on some GPUs, the first start's wait moved here) | ||
| }; | ||
| static readonly Dictionary<string, (double From, double Size)> WithoutShaders = new() | ||
| { | ||
| ["download"] = (0, 0.04), ["read"] = (0.04, 0.04), ["lift"] = (0.08, 0.14), ["compile"] = (0.22, 0.74), ["finish"] = (0.96, 0.04), | ||
| }; | ||
| Dictionary<string, (double From, double Size)> Weight = WithoutShaders; | ||
| // (this build prepares the shaders: the tick for a build, the saved choice for an update. The game's @@stage shaders came, and | ||
| // make_game's line when it left them to the first start: the row's end says which) | ||
| bool prepareNow, shadersBegan; | ||
| string? shadersLater; | ||
| TimeSpan? builtIn; // (at @@times: the build without the shaders' minutes, as build_times.json keeps it) | ||
|
|
||
| void MakeRows(bool update) | ||
| void MakeRows(bool update, bool shaders) | ||
| { | ||
| StageList.Children.Clear(); | ||
| rows.Clear(); | ||
| var list = Stages.Where(s => update || s.Id != "download").ToList(); | ||
| Weight = shaders ? WithShaders : WithoutShaders; | ||
| var list = Stages.Where(s => (update || s.Id != "download") && (shaders || s.Id != ShaderStep.Id)).ToList(); | ||
| for (var i = 0; i < list.Count; i++) | ||
| { | ||
| var row = new StepRow(i + 1, update ? list[i].Update : list[i].Build, last: i == list.Count - 1); | ||
|
|
@@ -752,7 +759,7 @@ void Run(Tools tools, List<string> args, bool keepRows = false, bool dlss = fals | |
| if (!keepRows) | ||
| { | ||
| updating = false; | ||
| MakeRows(update: false); | ||
| MakeRows(update: false, shaders: prepareNow); | ||
| SetStep(1); | ||
| BuildTitle.Text = "Building your game"; | ||
| UpdateBar.Visibility = Visibility.Collapsed; | ||
|
|
@@ -769,6 +776,9 @@ void Run(Tools tools, List<string> args, bool keepRows = false, bool dlss = fals | |
| compileEta = -1; | ||
| thisCompile = 0; | ||
| previousTotal = -1; | ||
| shadersBegan = false; | ||
| shadersLater = null; | ||
| builtIn = null; | ||
| ShowPage(BuildPage); | ||
| CancelButton.Visibility = Visibility.Visible; | ||
| SetProgress(updating ? Weight["read"].From : 0); | ||
|
|
@@ -795,6 +805,7 @@ void Line(string line) | |
| current = id; | ||
| if (rows.TryGetValue(id, out var row)) row.Begin(); | ||
| if (id == "compile") compileClock.Restart(); | ||
| if (id == ShaderStep.Id) shadersBegan = true; | ||
| if (Weight.TryGetValue(id, out var w)) SetProgress(w.From); | ||
| } | ||
| else if (line.StartsWith("@@progress ")) | ||
|
|
@@ -813,6 +824,7 @@ void Line(string line) | |
| return; | ||
| } | ||
| else if (line.StartsWith("@@done ")) finished = line[7..]; | ||
| else if (ShaderStep.Reason(line) is { } later) shadersLater = later; | ||
| else if (FilesLine.Match(line) is { Success: true } m) filesOf = int.Parse(m.Groups[1].Value); | ||
| else if (FileLine.IsMatch(line)) filesDone++; | ||
| if (current == "compile" && filesChanged < 0 && filesOf > 0 && rows.TryGetValue("compile", out var c)) | ||
|
|
@@ -843,6 +855,7 @@ void Number(string line) | |
| compileEta = eta; | ||
| break; | ||
| case "@@times" when p.Length == 4: | ||
| builtIn = elapsed.Elapsed; | ||
| long.TryParse(p[2], out thisCompile); | ||
| long.TryParse(p[3], out previousTotal); | ||
| break; | ||
|
|
@@ -916,7 +929,12 @@ void Ended(string? failure) | |
| runner = null; | ||
| if (finished.Length > 0) | ||
| { | ||
| foreach (var r in rows.Values) r.Done(); | ||
| foreach (var (id, r) in rows) | ||
| { | ||
| if (id != ShaderStep.Id) r.Done(); | ||
| else if (ShaderStep.Skipped(shadersBegan, shadersLater) is { } why) { r.Skipped(); r.Small(why); } | ||
| else r.Done(); | ||
| } | ||
| SetProgress(1); | ||
| ShowReady(); | ||
| return; | ||
|
|
@@ -957,7 +975,7 @@ void ShowReady() | |
| SetStep(2); | ||
| ReadyTitle.Text = updating ? "Your game is updated" : "Your game is ready"; | ||
| var compiledNote = thisCompile > 0 ? $" (compiled in {Span(TimeSpan.FromSeconds(thisCompile))})" : ""; | ||
| ReadyNote.Text = $"{(updating ? "Updated" : "Built")} in {Span(elapsed.Elapsed)}{compiledNote}{Against(elapsed.Elapsed)} · {finished}"; | ||
| ReadyNote.Text = $"{(updating ? "Updated" : "Built")} in {Span(elapsed.Elapsed)}{compiledNote}{Against(builtIn ?? elapsed.Elapsed)} · {finished}"; | ||
| if (!updating && Environment.GetEnvironmentVariable("HOOKSHOT_UI_PREVIEW") == null) | ||
| Finder.Shortcuts(finished, DesktopTick.IsChecked == true, StartTick.IsChecked == true); | ||
| else if (updating && Environment.GetEnvironmentVariable("HOOKSHOT_UI_PREVIEW") == null) | ||
|
|
@@ -1132,7 +1150,8 @@ public void BeginUpdate(string folder, string? tag) | |
| StepPill.Visibility = Visibility.Collapsed; | ||
| VersionChip.Visibility = Visibility.Visible; | ||
| VersionChipText.Text = from != null && from != to ? $"{from} → {to}" : to; | ||
| MakeRows(update: true); | ||
| prepareNow = BuilderSettings.PrepareNow(SettingsPath); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This reads the saved choice correctly, but on the launcher path |
||
| MakeRows(update: true, shaders: prepareNow); | ||
| BuildTitle.Text = "Updating your game"; | ||
| BuildNote.Text = "Getting the update…"; | ||
| RingBox.Visibility = Visibility.Collapsed; | ||
|
|
@@ -1173,7 +1192,9 @@ internal void UpdateBuild(Tools tools, string folder) | |
| // (a folder built with NVIDIA DLSS, its license accepted then, is built with it again) | ||
| var dlss = File.Exists(System.IO.Path.Combine(folder, "ngx", "nvngx_dlss.dll")) && | ||
| File.Exists(System.IO.Path.Combine(Storage.NgxSdk, "LICENSE.txt")); | ||
| Run(tools, new List<string> { "--update", folder }, keepRows: true, dlss: dlss); | ||
| // (prepareNow: the saved choice, read in BeginUpdate. The launcher's Update and the --update start after a self-update never | ||
| // ran ApplySettings, so the tick there is only its default) | ||
| Run(tools, ShaderStep.Args(new List<string> { "--update", folder }, prepareNow), keepRows: true, dlss: dlss); | ||
| } | ||
|
|
||
| // ================= the window ================= | ||
|
|
@@ -1288,6 +1309,24 @@ public void Done() | |
| time.Foreground = B("InkFaint"); | ||
| } | ||
|
|
||
| /// (not done: left to later, the shaders' step when they were not prepared now) | ||
| public void Skipped() | ||
| { | ||
| if (running) Tick(); | ||
| running = false; | ||
| watch.Stop(); | ||
| disc.Fill = Brushes.Transparent; | ||
| disc.Stroke = B("GlassLineStrong"); | ||
| disc.StrokeThickness = 1.5; | ||
| halo.Fill = Brushes.Transparent; | ||
| number.Text = "–"; | ||
| number.Foreground = B("InkFaint"); | ||
| tick.Visibility = Visibility.Collapsed; | ||
| name.Foreground = B("InkFaint"); | ||
| name.FontWeight = FontWeights.Medium; | ||
| time.Foreground = B("InkFaint"); | ||
| } | ||
|
|
||
| public void Stopped() | ||
| { | ||
| running = false; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| using System.Text.RegularExpressions; | ||
|
|
||
| namespace HookshotBuilder; | ||
|
|
||
| /// The build's last step, the shaders' preparation (nx2/make_game.py prepare_shaders): the argument that leaves it to the game's | ||
| /// first start, and what its row says once the build is done | ||
| static class ShaderStep | ||
| { | ||
| public const string Id = "shaders"; | ||
| public const string Later = "--no-prepare-shaders"; | ||
| // (make_game.py NOT_PREPARED: a plain line, so a builder that does not know it only shows it) | ||
| static readonly Regex NotPrepared = new(@"^the shaders were not prepared now \((.*)\): "); | ||
|
|
||
| /// make_game's arguments with the player's choice (unticked: the game prepares its shaders at its first start) | ||
| public static List<string> Args(List<string> args, bool prepare) | ||
| { | ||
| if (!prepare) args.Add(Later); | ||
| return args; | ||
| } | ||
|
|
||
| /// Why make_game left the shaders to the first start, when this line says so; else null | ||
| public static string? Reason(string line) => NotPrepared.Match(line) is { Success: true } m ? m.Groups[1].Value : null; | ||
|
|
||
| /// The line under the row of a finished build when the shaders were not prepared, or null when they were (a check mark). | ||
| /// began: the game's @@stage shaders came; reason: make_game's not-prepared line | ||
| public static string? Skipped(bool began, string? reason) => | ||
| reason != null ? $"Left to the game's first start: {reason}" | ||
| : began ? null | ||
| : "Left to the game's first start"; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,7 +30,7 @@ | |
|
|
||
| CONFIG = os.path.join(os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config"), "hookshot", "builder.ini") | ||
| STAGES = {"read": "Reading your game files", "lift": "Translating the game to PC code", "compile": "Compiling", | ||
| "finish": "Putting the game folder together"} | ||
| "finish": "Putting the game folder together", "shaders": "Preparing shaders"} | ||
|
|
||
|
|
||
| def left_text(seconds): | ||
|
|
@@ -70,8 +70,7 @@ def play(folder): | |
| exe = linux_update.game_exe(folder) | ||
| if not exe: | ||
| return | ||
| env = {k: v for k, v in os.environ.items() if not k.startswith(("HOOKSHOT_", "PYTHON", "APPDIR", "APPIMAGE", "ARGV0", "OWD"))} | ||
| env.pop("LD_LIBRARY_PATH", None) | ||
| env = make_game.game_env() # (as make_game's shader preparation and hookshot-launch: the game's own SDL, the system's libraries) | ||
| env["HOOKSHOT_NO_UPDATE_CHECK"] = "1" | ||
| subprocess.Popen([exe], cwd=folder, env=env, start_new_session=True, | ||
| stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | ||
|
|
@@ -178,6 +177,7 @@ def __init__(self, launch=None, update=None, fetched=False, tag=None): | |
| names = [n for _, n in make_game.LANGUAGES] | ||
| code = self.cfg.get("language", "en-US") | ||
| self.language = tk.StringVar(value=dict(make_game.LANGUAGES).get(code, names[0])) | ||
| self.prepare = tk.BooleanVar(value=self.cfg.get("prepare", "1") != "0") # (make_game.py's last step: off, at the first start) | ||
| self.check_text = tk.StringVar() | ||
| self.space_text = tk.StringVar() | ||
| self.stage_text = tk.StringVar() | ||
|
|
@@ -245,6 +245,9 @@ def show_form(self): | |
| ttk.Combobox(f, textvariable=self.language, values=[n for _, n in make_game.LANGUAGES], state="readonly").grid( | ||
| row=row, column=1, sticky="w", **self.pad) | ||
| row += 1 | ||
| ttk.Checkbutton(f, text="Prepare shaders now (unticked: at the game's first start, behind a black window)", | ||
| variable=self.prepare).grid(row=row, column=0, columnspan=3, sticky="w", **self.pad) | ||
| row += 1 | ||
| ttk.Label(f, text="Takes about 45 minutes and needs 60 GB.").grid(row=row, column=0, columnspan=3, sticky="w", **self.pad) | ||
| row += 1 | ||
| ttk.Label(f, textvariable=self.space_text).grid(row=row, column=0, columnspan=3, sticky="w", **self.pad) | ||
|
|
@@ -414,7 +417,8 @@ def start_make_update(self, folder): | |
| self.status.set("This builder is the latest version.") | ||
| self.lookup_latest() | ||
| return | ||
| cmd = [sys.executable, "-I", os.path.join(ROOT, "nx2", "make_game.py"), "--update", folder] | ||
| cmd = [sys.executable, "-I", os.path.join(ROOT, "nx2", "make_game.py"), "--update", folder] + \ | ||
| ([] if self.prepare.get() else ["--no-prepare-shaders"]) # (as the last build chose) | ||
| self.heading.configure(text="Updating your game") | ||
| self.stage_text.set("Compiling what changed") | ||
| self.started = time.time() | ||
|
|
@@ -458,12 +462,13 @@ def build(self): | |
| except ValueError as error: | ||
| return messagebox.showerror("Hookshot", str(error)) | ||
| code = next((c for c, n in make_game.LANGUAGES if n == self.language.get()), "en-US") | ||
| self.cfg.update(kind=self.kind.get(), nup=self.nup.get(), romfs=self.romfs.get(), exefs=self.exefs.get(), out=out, language=code) | ||
| self.cfg.update(kind=self.kind.get(), nup=self.nup.get(), romfs=self.romfs.get(), exefs=self.exefs.get(), out=out, language=code, | ||
| prepare="1" if self.prepare.get() else "0") | ||
| save_config(self.cfg) | ||
| # (issue 531) Without HOOKSHOT_JOBS or RX_JOBS, make_game chooses as on Windows, from the memory free now; this window chose | ||
| # fewer, 80% of the total memory at 3.5 GB each, and the CPU sat at 29% | ||
| cmd = [sys.executable, "-I", os.path.join(ROOT, "nx2", "make_game.py")] + src + ["--out", out, "--language", code] + \ | ||
| ([] if jobs is None else ["--jobs", str(jobs)]) | ||
| ([] if jobs is None else ["--jobs", str(jobs)]) + ([] if self.prepare.get() else ["--no-prepare-shaders"]) | ||
| self.show_progress() | ||
| self.started = time.time() | ||
| self.proc = subprocess.Popen(cmd, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, | ||
|
|
@@ -482,6 +487,7 @@ def show_progress(self, title="Building"): | |
| self.eta = None | ||
| self.compile_started = None | ||
| self.times = None | ||
| self.shaders_later = False | ||
| f = self.page = ttk.Frame(self) | ||
| f.pack(fill="both", expand=True, pady=self.px(8)) | ||
| f.columnconfigure(0, weight=1) | ||
|
|
@@ -547,8 +553,9 @@ def on_line(self, line): | |
| stage = line.split(" ", 2)[1] | ||
| self.stage_text.set(STAGES.get(stage, line.split(" ", 2)[-1])) | ||
| self.bar.configure(mode="determinate", value=0) | ||
| if stage == "compile": | ||
| self.compile_started = time.time() | ||
| # (the compile's clock and countdown are the compile's: a later step, the minutes-long shaders one, without them) | ||
| self.compile_started = time.time() if stage == "compile" else None | ||
| self.eta = None | ||
| elif line.startswith("@@progress "): | ||
| try: | ||
| self.bar.configure(value=float(line.split()[1])) | ||
|
|
@@ -591,6 +598,8 @@ def on_line(self, line): | |
| self.built = line[7:] | ||
| elif line.startswith("@@error "): | ||
| self.error = line[8:] | ||
| elif line.startswith(make_game.NOT_PREPARED): | ||
| self.shaders_later = True | ||
| if hasattr(self, "log"): | ||
| self.log.insert("end", line + "\n") | ||
| self.log.see("end") | ||
|
|
@@ -608,6 +617,8 @@ def on_exit(self, code): | |
| text += ", compiled in %d:%02d" % (int(compiled) // 60, int(compiled) % 60) | ||
| if previous > 0: | ||
| text += " · last time %d:%02d" % (int(previous) // 3600, int(previous) // 60 % 60) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. minor: on Linux "built in" is wall time including the preparation, while "last time" is the |
||
| if self.shaders_later: | ||
| text += " · the shaders are prepared at its first start" | ||
| self.stage_text.set(text) | ||
| self.bar.configure(value=1.0) | ||
| try: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A Cancel during the optional shaders step goes through the
"cancelled"branch just below, back to the files page, even though the new game is already installed by then. SoShowReadynever runs: no shortcuts, and the launcher doesn't learn the folder is ready. On an AMD GPU where preparation takes 20+ minutes, pressing Cancel there is quite likely. Linux has the same issue (on_exitshows "The build stopped"). Ending a cancel that comes after the finish stage as "ready, shaders left to the first start" would fit the step being optional.