Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion gpu/vk/vk_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ enum { HOS_OPT_GYRO = 18, HOS_OPT_GYRO_SENS_X, HOS_OPT_GYRO_INV_X, HOS_OPT_GYRO_
/* (hookshot) NVIDIA DLSS's camera jitter (host/rx_backend.c, gpu/vk/vk_dlss.cpp): the scene's width and height while DLSS
draws, 0 when there is to be no jitter, and the jitter sequence's length. 24 on: 18 to 22 are the motion rows' */
enum { HOS_OPT_DLSS_JITTER_W = 24, HOS_OPT_DLSS_JITTER_H, HOS_OPT_DLSS_JITTER_PHASES };
/* (hookshot) the Audio tab's rows (host/nn_audio.c reads them every 100 ms): the volume in percent, and no sound while
another window has the focus */
enum { HOS_OPT_VOLUME = 27, HOS_OPT_MUTE_BACKGROUND };

/* (hookshot) the controls' names (host/nn_hid.c, host/pad_sdl.c, the pad_ and key_ lines of hookshot_options.ini):
the Switch's buttons in nn::hid's bit order, and a gamepad's inputs by position, SDL's order (the bottom face
Expand Down Expand Up @@ -154,7 +157,8 @@ typedef struct MkwVkRenderer {
int (*option)(int id); /* (hookshot, the Insert menu) 0: game time follows the clock, 1: the menu is open, 2: dynamic resolution, 3: its fixed scale in percent, 4: the controllers count behind other windows, 5: text speed (0 normal to 3 instant), 6: native mouse look (host/mouse_native.c),
7: the mouse's sensitivity in percent, 8: its up and down swapped (host/nn_hid.c),
18: motion controls from a pad's gyroscope (host/pad_sdl.c), 19 and 22: their horizontal
and vertical sensitivity in percent, 20 and 21: each turned around */
and vertical sensitivity in percent, 20 and 21: each turned around,
27: the volume in percent, 28: no sound while another window has the focus (host/nn_audio.c) */
/* a submit with its queue */
void (*nvn_submit_q)(const uint64_t *handles, int n, uint64_t queue);
/* (hookshot) a fence of one queue: the value its own last submission signals (nvnQueueFenceSync) */
Expand Down
36 changes: 23 additions & 13 deletions gpu/vk/vk_menu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ namespace {

enum MenuTab {
TAB_GRAPHICS = 0,
TAB_GAMEPLAY = 1,
TAB_CONTROLS = 2,
TAB_COUNT = 3
TAB_AUDIO = 1,
TAB_GAMEPLAY = 2,
TAB_CONTROLS = 3,
TAB_COUNT = 4
};

struct Row {
Expand Down Expand Up @@ -477,6 +478,14 @@ Row rows[] = {
"Bounds-checked buffer reads: a read past a buffer's end is dropped instead of stopping the GPU. Applies after a "
"restart.", hn_robust, true},

// Audio (2 rows)
{TAB_AUDIO, "Volume", &opts.volume, 0, 100, 5, tx_percent,
"How loud the game plays, in percent of its own volume. The game's own sound settings and Windows' volume still apply.",
nullptr, false, true},
{TAB_AUDIO, "Mute in background", &opts.mute_background, 0, 1, 1, tx_onoff,
"Silences the game while another window has the focus. The sound comes back when you switch to the game.", nullptr,
false, true},

// Gameplay (3 rows)
{TAB_GAMEPLAY, "FPS counter", &opts.show_fps, 0, 1, 1, tx_onoff, "Frames per second in the top right corner.", hn_fps,
false, true},
Expand Down Expand Up @@ -612,7 +621,7 @@ constexpr int kRows = sizeof rows / sizeof rows[0];
constexpr int kMenuWidth = menu_hit::kWidth; // (at a scale of 1: 720 lines)

int cur_tab = TAB_GRAPHICS;
int tab_selected[TAB_COUNT] = {0, 0, 0};
int tab_selected[TAB_COUNT] = {};

// ---- the rows that take a restart: their values at the start (menu_init, the GPU's at menu_set_gpu_count) ----
namespace mr = menu_restart;
Expand Down Expand Up @@ -705,6 +714,7 @@ constexpr int64_t kMarqueeStep = 150000000LL, kMarqueeRest = 1200000000LL; //
// (hookshot) the mouse (menu_tick): the tab strip's part under the cursor (a tab 0 to 2, 3 the previous, 4 the next; -1
// none); paint_menu's record of each shown row's value's left edge (as with its arrows, whether shown or not) and of the
// rows a slider's track was drawn on; the slider held with the left button (its row in the tab, -1 none)
constexpr int kHoverPrev = TAB_COUNT, kHoverNext = TAB_COUNT + 1; // (hover_tab: the strip's ends)
int hover_tab = -1;
int row_value_l[kRows];
bool row_slider[kRows];
Expand All @@ -713,9 +723,9 @@ bool mouse_read = false; // (the menu has read the mouse since it opened)
// (the Cheats page's and the list pages' record of each shown row's or line's value's left edge: the menu's width, none)
int cheat_value_l[kCheatRows];
int list_value_l[kTeleportLines];
bool is_slider(const Row& r) { return r.value == &opts.sharpness || r.value == &opts.mouse_sens || r.value == &opts.gyro_sens_x || r.value == &opts.gyro_sens_y; }
bool is_slider(const Row& r) { return r.value == &opts.sharpness || r.value == &opts.mouse_sens || r.value == &opts.gyro_sens_x || r.value == &opts.gyro_sens_y || r.value == &opts.volume; }
/// (a scale: the arrows, a click and the wheel stop at its ends; Rumble strength would go from Off to its strongest)
bool stops_at_ends(const Row& r) { return r.value == &opts.mouse_sens || r.value == &opts.rumble || r.value == &opts.gyro_sens_x || r.value == &opts.gyro_sens_y; }
bool stops_at_ends(const Row& r) { return r.value == &opts.mouse_sens || r.value == &opts.rumble || r.value == &opts.gyro_sens_x || r.value == &opts.gyro_sens_y || r.value == &opts.volume; }
/// (the ini's Rumble strength on the menu's steps: 0 to 150, the nearest 25)
int rumble_step(int v) { return (std::min(std::max(v, 0), 150) + 12) / 25 * 25; }
std::string ini_path() {
Expand Down Expand Up @@ -1298,24 +1308,24 @@ void paint_menu(HDC dc, uint32_t w, uint32_t h, int s) {
DeleteObject(body);
return;
}
// the tab strip: Graphics, Gameplay, Controls (Q and E, the bumpers or Tab change it: menu_tick)
// the tab strip: Graphics, Audio, Gameplay, Controls (Q and E, the bumpers or Tab change it: menu_tick)
const int tab_y = menu_hit::kTabY * s;
const int tab_h = menu_hit::kTabH * s;
fill(dc, 0, tab_y, (int)w, tab_h, RGB(22, 28, 25));
HFONT small_font = font(12 * s, FW_SEMIBOLD);
SelectObject(dc, small_font);
SetTextColor(dc, hover_tab == 3 ? RGB(210, 225, 215) : RGB(110, 135, 120)); // (lighter under the cursor: a click goes there)
SetTextColor(dc, hover_tab == kHoverPrev ? RGB(210, 225, 215) : RGB(110, 135, 120)); // (lighter under the cursor: a click goes there)
TextOutA(dc, 16 * s, tab_y + 8 * s, "[Q / LB]", 8);
const char* rb_hint = "[E / RB]";
SIZE rbsz{};
GetTextExtentPoint32A(dc, rb_hint, (int)strlen(rb_hint), &rbsz);
SetTextColor(dc, hover_tab == 4 ? RGB(210, 225, 215) : RGB(110, 135, 120));
SetTextColor(dc, hover_tab == kHoverNext ? RGB(210, 225, 215) : RGB(110, 135, 120));
TextOutA(dc, (int)w - 16 * s - rbsz.cx, tab_y + 8 * s, rb_hint, (int)strlen(rb_hint));
HFONT tab_font = font(13 * s, FW_BOLD);
SelectObject(dc, tab_font);
const char* const tab_titles[TAB_COUNT] = {"GRAPHICS", "GAMEPLAY", "CONTROLS"};
const char* const tab_titles[TAB_COUNT] = {"GRAPHICS", "AUDIO", "GAMEPLAY", "CONTROLS"};
const int center_start = menu_hit::kTabSide * s;
const int tab_slot_w = menu_hit::tab_slot_w(s, (int)w);
const int tab_slot_w = menu_hit::tab_slot_w(s, (int)w, TAB_COUNT);
for (int t = 0; t < TAB_COUNT; t++) {
const bool active = (t == cur_tab);
int slot_x = center_start + t * tab_slot_w;
Expand Down Expand Up @@ -2112,8 +2122,8 @@ void menu_tick() {
if (mouse) { // (the mouse: see the top of this file)
const int count = tab_row_count(cur_tab);
const menu_hit::Hit hit = menu_hit::options(mx, my, scale, mw, mh, TAB_COUNT, count, row_value_l, row_slider);
const int ht = hit.part == menu_hit::kTab ? hit.index : hit.part == menu_hit::kTabPrev ? 3
: hit.part == menu_hit::kTabNext ? 4 : -1;
const int ht = hit.part == menu_hit::kTab ? hit.index : hit.part == menu_hit::kTabPrev ? kHoverPrev
: hit.part == menu_hit::kTabNext ? kHoverNext : -1;
if (ht != hover_tab) hover_tab = ht, changed = true;
int& sel = tab_selected[cur_tab];
const bool on_row = hit.part == menu_hit::kRow || hit.part == menu_hit::kSlider;
Expand Down
2 changes: 2 additions & 0 deletions gpu/vk/vk_menu.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ struct Options {
int preset = 0; // quality preset: 0 Custom, 1 Minimum (4GB), 2 Low (6GB), 3 Balanced (8GB), 4 High (12GB), 5 Ultra (16GB+)
int vram_mb = 0; // dynamic texture cache budget limit in MB
int mirror_mb = 0; // staging mirror limit in MB
int volume = 100; // (hookshot) the game's sound in percent, 0 to 100 (host/nn_audio.c: HOS_OPT_VOLUME, 27)
int mute_background = 0; // (hookshot) no sound while another window has the focus (host/nn_audio.c: HOS_OPT_MUTE_BACKGROUND, 28)
int mic = 1; // (hookshot) the ocarina's microphone: 0 Off, 1 the system's default, 2.. a device as host->mic_name lists it (host/nn_audio_in.c)
};
extern Options opts;
Expand Down
8 changes: 5 additions & 3 deletions gpu/vk/vk_menu_hit.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ namespace mkwvk::menu_hit {
// ---- the options page, at a scale of 1 (the window 720 pixels high; 1440 and up: 2, and so on) ----
constexpr int kWidth = 480;
constexpr int kTabY = 44, kTabH = 32; // the tab strip
constexpr int kTabSide = 85; // either side of the tab titles: [Q / LB] and [E / RB], the previous and next tab
constexpr int kTabSide = 70; // either side of the tab titles: [Q / LB] and [E / RB], the previous and next tab
// (their text ~50 from 16 in; 70 leaves the four tabs slots of 85, room for GAMEPLAY's
// and CONTROLS' highlight at 13 px bold)
constexpr int kRowGap = 8, kRowH = 28; // the rows, from kTabY + kTabH + kRowGap
constexpr int kRowX = 8; // a row's band, kRowX in from either side
constexpr int kValueRight = 18; // a value ends kValueRight from the right
Expand Down Expand Up @@ -57,7 +59,7 @@ inline void origin(int img_w, int img_h, int menu_w, int menu_h, int* x0, int* y
*y0 = (img_h - menu_h) / 2;
}
inline int rows_top(int s) { return (kTabY + kTabH + kRowGap) * s; }
inline int tab_slot_w(int s, int w) { return (w - 2 * kTabSide * s) / 3; }
inline int tab_slot_w(int s, int w, int tabs) { return tabs > 0 ? (w - 2 * kTabSide * s) / tabs : 0; }
/// A slider's value for a point along its track (lo to hi in steps of step), nearest step, held to its ends
inline int slider_value(int permille, int lo, int hi, int step) {
if (step <= 0 || hi <= lo) return lo;
Expand All @@ -79,7 +81,7 @@ inline Hit options(int x, int y, int s, int w, int h, int tabs, int rows, const
Hit hit;
if (x < 0 || y < 0 || x >= w || y >= h) return hit.part = kOutside, hit;
if (y >= kTabY * s && y < (kTabY + kTabH) * s) {
const int side = kTabSide * s, slot = tab_slot_w(s, w);
const int side = kTabSide * s, slot = tab_slot_w(s, w, tabs);
if (x < side) hit.part = kTabPrev;
else if (x >= w - side) hit.part = kTabNext;
else if (slot > 0) {
Expand Down
3 changes: 2 additions & 1 deletion gpu/vk/vk_replay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3738,7 +3738,8 @@ void fill_replay_table(MkwVkRenderer& t) {
return id == 0 ? opts.realtime : id == 1 ? menu_is_open() : id == 2 ? resolution_auto() && !dlss_full_scale() : id == 3 ? (dlss_full_scale() ? 10000 : resolution_scale()) : id == 4 ? opts.pad_background : id == 5 ? opts.text_speed : id == 6 ? opts.mouse_native :
id == 7 ? opts.mouse_sens : id == 8 ? opts.mouse_invert : id == 9 ? opts.rumble : id == 16 ? opts.invert_cam_x : id == 17 ? opts.invert_cam_y : id == HOS_OPT_GYRO ? opts.gyro : id == HOS_OPT_GYRO_SENS_X ? opts.gyro_sens_x : id == HOS_OPT_GYRO_SENS_Y ? opts.gyro_sens_y : id == HOS_OPT_GYRO_INV_X ? opts.gyro_inv_x : id == HOS_OPT_GYRO_INV_Y ? opts.gyro_inv_y :
id == HOS_OPT_DLSS_JITTER_W || id == HOS_OPT_DLSS_JITTER_H ? (int)dlss_jitter_size(id - HOS_OPT_DLSS_JITTER_W) :
id == HOS_OPT_DLSS_JITTER_PHASES ? (int)dlss_jitter_size(2) : 0;
id == HOS_OPT_DLSS_JITTER_PHASES ? (int)dlss_jitter_size(2) :
id == HOS_OPT_VOLUME ? opts.volume : id == HOS_OPT_MUTE_BACKGROUND ? opts.mute_background : 0;
};
t.option_set = menu_option_set;
t.nvn_register_sampler = register_sampler_queued;
Expand Down
56 changes: 56 additions & 0 deletions host/nn_audio.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
HOS_AUDIO_DEVICE=<n>: that waveOut device first; HOS_AUDIO_MUTE=1: the device fed at volume 0 (test runs);
HOS_AUDIO_CHANNELS=2 or 6: stereo or 5.1 whatever the default output has (see au_speakers). */
#include "hos.h"
#include "../gpu/vk/vk_api.h" /* (the Audio tab's options: HOS_OPT_VOLUME, HOS_OPT_MUTE_BACKGROUND) */
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
Expand Down Expand Up @@ -1003,6 +1004,57 @@ static int64_t audio_mix_deadline(int64_t now, int64_t next, int64_t period) {
return next;
}

/* ---- the player's volume (the Insert menu's Audio tab, gpu/vk/vk_menu.cpp) ------------------------------------- */
/* The frame's samples (frames x channels, interleaved) scaled from `from`, the gain the last frame ended at, to `want`,
step by step over the frame, so a change does not click. The gains are 0 to 1: nothing clips. Returns the gain the
frame ended at. tests/test_audio_volume.py */
static float audio_apply_gain(int16_t *pcm, int frames, int channels, float from, float want) {
if (from == 1.0f && want == 1.0f) return want;
for (int i = 0; i < frames; i++) {
const float g = from + (want - from) * (float)(i + 1) / (float)frames;
for (int c = 0; c < channels; c++) pcm[i * channels + c] = (int16_t)lrintf((float)pcm[i * channels + c] * g);
}
return want;
}

/* Whether the game's window counts as in front, as host/nn_hid.c's game_in_front decides it: the game's own window does,
another normal window does not, and an overlay (a tool or no-activate window: Steam's, Discord's, but also a toast or
a tray flyout) counts as the game's only while the last normal window in front was the game's. *last_normal_was_us
keeps that between calls. tests/test_audio_volume.py */
static int audio_front_decide(int ours, int overlay, int *last_normal_was_us) {
if (ours) return *last_normal_was_us = 1;
if (overlay && *last_normal_was_us) return 1;
*last_normal_was_us = 0;
return 0;
}
static int audio_window_in_front(void) { /* (the mixer thread's only: its own state, not game_in_front's) */
static int last_normal_was_us = 1;
DWORD pid = 0;
HWND w = GetForegroundWindow();
if (!w) return 0;
GetWindowThreadProcessId(w, &pid);
const int ours = pid == GetCurrentProcessId();
const int overlay = !ours && (GetWindowLongPtrA(w, GWL_EXSTYLE) & (WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE)) != 0;
return audio_front_decide(ours, overlay, &last_normal_was_us);
}

/* The Volume row's percent (0 to 100) as a gain (0 to 1), on a squared curve so the slider's steps sound about even: 50%
is a quarter, -12 dB. A straight line left the bottom half of the slider nearly all very quiet. */
static float audio_volume_curve(int percent) {
const float v = (float)(percent < 0 ? 0 : percent > 100 ? 100 : percent) / 100.0f;
return v * v;
}
/* The gain the menu asks for: Volume and Mute in background. With no menu (the WebGPU renderer) the game plays at full
volume. */
static float audio_player_gain(void) {
int rx_option(int id, int otherwise);
int volume = rx_option(HOS_OPT_VOLUME, 100);
if (volume < 0) volume = 0;
if (volume > 100) volume = 100;
if (volume && rx_option(HOS_OPT_MUTE_BACKGROUND, 0) && !audio_window_in_front()) volume = 0;
return audio_volume_curve(volume);
}

/* ---- the renderer thread --------------------------------------------------------------------------------------- */
static uint64_t renderer_event;
static volatile int renderer_running;
Expand All @@ -1015,6 +1067,8 @@ static DWORD WINAPI renderer_main(void *p) {
output_open();
int64_t period = 5000000, next = hos_now_ns() + period; /* one audio frame: 240 samples at 48 kHz */
static int16_t pcm[FRAME * OUT_MAX_CH];
float gain = 1.0f, gain_want = 1.0f;
int gain_frames = 0;
for (;;) {
hos_sleep_ns(next - hos_now_ns());
next = audio_mix_deadline(hos_now_ns(), next, period);
Expand All @@ -1023,6 +1077,8 @@ static DWORD WINAPI renderer_main(void *p) {
if (ncmds && hos_now_ns() - last_update_ns > 50000000) commit(); /* (no update came: see "parameter updates") */
render_frame(pcm, out_channels);
au_unlock();
if (gain_frames-- <= 0) gain_want = audio_player_gain(), gain_frames = 20; /* (the menu read every 100 ms) */
gain = audio_apply_gain(pcm, FRAME, out_channels, gain, gain_want);
output_frame(pcm);
elapsed_frames++;
if (renderer_event) hos_system_event_signal(renderer_event);
Expand Down
6 changes: 6 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ choices and starts the game again, so progress since your last save is lost.
| | Handheld profile | Off, On (after a restart) | Off |
| | Ultrawide | 16:9, Monitor's shape (after a restart) | 16:9 |
| | Robust buffers | Auto, Off, On (after a restart) | Auto |
| Audio | Volume | 0 to 100%, in steps of 5 | 100% |
| | Mute in background | Off, On | Off |
| Gameplay | FPS counter | Off, On | Off |
| | Game speed | Per frame, Real time | Real time |
| | Text speed | Normal, 2x, 4x, Instant | Normal |
Expand Down Expand Up @@ -277,6 +279,10 @@ choices and starts the game again, so progress since your last save is lost.
- **Handheld profile** runs the game as if the console were undocked: lower resolution, lighter on the GPU.
- **Robust buffers:** a read past a buffer's end is dropped instead of stopping the GPU. Auto is on, on every card; Off
can be faster on some NVIDIA cards.
- **Volume** scales the game's whole sound, on top of the game's own sound settings and Windows' volume. Its steps
follow a curve so they sound about even: 50% is a quarter of the level (-12 dB). **Mute in
background** silences the game while another window has the focus; an overlay such as Steam's or Discord's does not
count as another window.
- **Game speed:** with Per frame, the game runs slower whenever it is below 60 fps. Real time keeps it at normal speed
down to 15 fps.
- **FPS counter** shows in the top right corner. With frame generation on, it also shows the game's own rate.
Expand Down
1 change: 1 addition & 0 deletions tests/run_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"tests/test_startup_progress.py": ["--cxx", "g++"],
"tests/test_audio_voice_biquad.py": ["--cc", "gcc"],
"tests/test_motion.py": ["--cc", "gcc"],
"tests/test_audio_volume.py": ["--cc", "gcc"],
"tests/test_cpp_preflight.py": ["--cxx", "g++"],
"tests/test_invalidate_vcore.py": ["--cxx", "g++"],
"tests/test_linux_driver_fault_guard.py": ["--cxx", "{cxx}"],
Expand Down
Loading