mirror of
https://github.com/mihawk-99/PS5_RetroArch.git
synced 2026-10-18 23:09:29 -05:00
Frame generation plans on the flips the output really shows (0131)
The 59.94 Hz mode takes 120 flips a second on a VRR display and 60 on an
output without 120 Hz. 0121 assumed 119.88 Hz for every 59.94 Hz mode, so
on a 60 Hz output each picture was presented twice and games ran at half
speed with broken audio (a tester's v2.0.0-beta-2 logs: "display 119.880
Hz ... each shown 2x", 58.8 presents/s, the core at 29.6 calls/s).
Patch 0131. khr_display_ctx.c times flips once per process, on the first
swapchain, where the mode is below 100 Hz: the median of nine acquire
intervals once every image is queued, 1 s timeouts, and on a failure a
warning and the mode is believed. vulkan_common.c keeps the rate. The plan
in retroarch.c takes 119.88 only above 100 Hz, and a Target Framerate
above the display's rate becomes the display's (120 on a 60 Hz output is
60, and a 30 fps game still gets a frame made between its frames).
Evidence: evidence/framegen-flip-rate (python3 tools/evidence.py compare
evidence: 128 replayed, 0 failed). tests/test_framegen_plan.py composes
the plan from EDITS as the script applies it and runs nine cases; with
0131's edits left out it fails on the 60 Hz case. The eboot.bin built from
this tree (identity 1f81446a..., sha256 d3dc2a31...) ran on the tester's
60 Hz output, and the owner reports it worked perfectly; no log of that run
was received. Not run yet: the owner's base PS5 with its VRR display (the
console did not answer).
format PASS; evidence PASS; unit 192 of 197 OK. The 5 errors are tests
that read .deps/{ppsspp,azahar,desmume}-src, deleted with .deps on
2026-10-15 along with every core source the build gate's catalog step
reads.
This commit is contained in:
1 parent
7d45412b4b
commit
5c4256b202
5 files changed
+282
No files matched your search
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"step": "framegen-flip-rate",
|
||||
"recorded": "2026-10-17T21:00:57Z",
|
||||
"console_run": "",
|
||||
"revision": "7d45412 (v2.0.0-beta-2) with patch 0131, the commit this record lands in",
|
||||
"command": "PATH=<scratch python3 wrapper that keeps build/webui-core-metadata>:$PATH PS5_REUSE_STAGED_CORES='<the 33 cores>' bash tools/build-title.sh; sha256sum dist/PPSA99169/eboot.bin; grep -a -c -F '<each format string>' dist/PPSA99169/eboot.bin; python3 -m unittest tests.test_framegen_plan",
|
||||
"raw_capture": "klog/framegen-flip-rate/record.txt",
|
||||
"raw_lines": 21,
|
||||
"raw_bytes": 2367,
|
||||
"lines": [
|
||||
"Hotfix eboot.bin: build identity 1f81446ad708fb08f7dfbbb5a4239afd69b49e256b77b2b630ec5a95dd27dce3, sha256 d3dc2a31ab7e66ae0a8431a0706d214eec7787585c3bcc09cdf19e67e9badba6.",
|
||||
"Hotfix zip: PS5_RetroArch-v2.0.0-beta-2-hotfix1.zip, 12218002 bytes, sha256 53f8902c7bf4efc9732f1b74c9381804bfb6e1b94f0cfd56ce0aa7c016596517; PPSA99169/eboot.bin only, over v2.0.0-beta-2.",
|
||||
"eboot.bin carries (grep -a -c -F, once each): [PS5] The output shows %.3f flips a second in its %.3f Hz mode.",
|
||||
"eboot.bin carries: [PS5] Flips timed %.2f to %.2f ms apart, median %.2f ms.",
|
||||
"eboot.bin carries: [PS5] Flips could not be timed (VkResult %d).",
|
||||
"Before, a tester's 60 Hz output (v2.0.0-beta-2, build identity b285ff74):",
|
||||
" wsi/videoout: 119.88 Hz not offered on the console's present output (0); presenting at 59.94 Hz",
|
||||
" [INFO] [PS5] Display runs at 59.940 Hz; video_refresh_rate follows it (was 119.880).",
|
||||
" [INFO] [FrameGen] Target Framerate 60: display 119.880 Hz, game 58.944 fps -> 1.02 pictures per game frame, each shown 2x",
|
||||
" [INFO] [FrameGen] pace: 58.8 presents/s, 0.0 generated/s, 1.5 new frames/s, core 29.6 calls/s, governor level 0 (0.01 ms a picture)",
|
||||
" Across four logs: 64 two-second windows near 30 core calls/s and 46 between 35 and 55.",
|
||||
"Before, the owner's base PS5 (1440p VRR display, v2.0.0-beta-2, klog 210908):",
|
||||
" [AvControl] 119.88Hz request is converted to VRR 119.88Hz",
|
||||
" [AvControl] video: 1440P_11988 with VRR(peg:120 range:48 - 120); after the driver's restore VRR(peg:60 range:48 - 120), still 1440P_11988",
|
||||
" [INFO] [PS5] Display runs at 59.940 Hz; video_refresh_rate follows it (was 119.880).",
|
||||
" [INFO] [FrameGen] pace: 118.8 presents/s, 59.9 generated/s, 59.4 new frames/s, core 59.4 calls/s, governor level 0 (3.28 ms a picture)",
|
||||
" Same console and driver, a PS5_FrameGen run (2026-10-11): wsi/videoout: 119.88 Hz accepted, but a vblank comes every 20.877 ms; 59.94 Hz restored",
|
||||
"Host: python3 -m unittest tests.test_framegen_plan: OK. With 0131's edits left out it fails at plan(59940, 59940, 120, 58.944, &refresh) == 1.",
|
||||
"Gates: format PASS; evidence PASS; unit 192 of 197, the 5 errors being tests that read .deps/{ppsspp,azahar,desmume}-src, deleted with .deps on 2026-10-15.",
|
||||
"Owner: the tester's run of the hotfix worked perfectly (2026-10-17). No log of that run was received.",
|
||||
"Not run: 0131 on the owner's base PS5 (its VRR display); the console did not answer on 2026-10-17."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"step": "framegen-flip-rate",
|
||||
"note": "The 59.94 Hz mode takes 120 flips a second with VRR and 60 on an output without 120 Hz; 0121 planned 119.88 for both and halved the game's speed on the second. 0131 times the flips once and plans on them. Accepted on the owner's report of a tester's run on a 60 Hz output; the VRR path with 0131 is not yet run on a console.",
|
||||
"must_contain": [
|
||||
"build identity 1f81446ad708fb08f7dfbbb5a4239afd69b49e256b77b2b630ec5a95dd27dce3",
|
||||
"[PS5] The output shows %.3f flips a second in its %.3f Hz mode.",
|
||||
"119.88 Hz not offered on the console's present output (0)",
|
||||
"each shown 2x",
|
||||
"core 29.6 calls/s",
|
||||
"pace: 118.8 presents/s",
|
||||
"python3 -m unittest tests.test_framegen_plan: OK",
|
||||
"Owner: the tester's run of the hotfix worked perfectly",
|
||||
"Not run: 0131 on the owner's base PS5"
|
||||
],
|
||||
"must_not_contain": [
|
||||
"0131 verified on the VRR console"
|
||||
],
|
||||
"human_check": "Owner reports the tester's run of the hotfix (identity 1f81446a) on a 60 Hz output worked perfectly (2026-10-17); no log of that run was received."
|
||||
}
|
||||
@@ -281,3 +281,5 @@
|
||||
0129 HTTPS refuses a server whose certificate does not chain to the bundled roots (cacert.h): MBEDTLS_SSL_VERIFY_REQUIRED instead of upstream's optional check whose result was ignored, so nobody on the network path can stand in for RetroAchievements.
|
||||
|
||||
0130 A RetroAchievements sign-in that returns a token also removes the password from /app0/config/webui.cfg (with the empty token the WebUI put beside it) and from retroarch.cfg, which keeps its token (src/ps5_cheevos.c).
|
||||
|
||||
0131 Frame generation plans on the rate the output really shows flips at: the display context times flips once on its first swapchain where the mode says 59.94 Hz (VRR shows 120 a second there, an output without 120 Hz shows 60), the plan assumes 119.88 only where they came that fast, and a target above the display's rate is the display's rate.
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Run frame generation's plan (patches 0121 and 0131) as the port inserts it, for each kind of output."""
|
||||
import importlib.util
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
class FramegenPlan(unittest.TestCase):
|
||||
def test_the_plan_follows_the_flips_the_output_shows(self):
|
||||
spec = importlib.util.spec_from_file_location('patches', ROOT / 'tools/apply-port-patches.py')
|
||||
patches = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(patches)
|
||||
# The plan as 0121 inserts it, with every later retroarch.c edit applied in order, as main() does.
|
||||
edits = [e for e in patches.EDITS if e[0] == 'retroarch.c']
|
||||
start = next(i for i, e in enumerate(edits) if e[3] == 'Output Rate Auto: the presents per game frame')
|
||||
text = edits[start][2]
|
||||
for _, anchor, replacement, marker in edits[start + 1:]:
|
||||
if marker not in text and anchor in text:
|
||||
text = text.replace(anchor, replacement, 1)
|
||||
plan = text[:text.index(' unsigned shader_subframes')]
|
||||
source = r'''
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
#define RARCH_LOG(...) ((void)0)
|
||||
typedef struct {
|
||||
struct { bool video_framegen, video_framegen_auto; } bools;
|
||||
struct { unsigned video_framegen_target, video_shader_subframes; } uints;
|
||||
struct { float video_refresh_rate; } floats;
|
||||
} settings_t;
|
||||
unsigned vulkan_ps5_display_refresh_x1000, vulkan_ps5_flip_rate_x1000, ps5_fg_dup;
|
||||
float ps5_fg_core_fps, ps5_fg_target_fps;
|
||||
/* The presents per game frame for a mode and the flips timed in it; refresh: what RetroArch then paces on. */
|
||||
static unsigned plan(unsigned mode, unsigned flips, unsigned target, double input_fps, float *refresh)
|
||||
{
|
||||
settings_t s = {{true, true}, {target, 1}, {mode / 1000.0f}};
|
||||
settings_t *settings = &s;
|
||||
float video_refresh_rate = settings->floats.video_refresh_rate;
|
||||
vulkan_ps5_display_refresh_x1000 = mode;
|
||||
vulkan_ps5_flip_rate_x1000 = flips;
|
||||
''' + plan + r'''
|
||||
(void)video_refresh_rate;
|
||||
*refresh = settings->floats.video_refresh_rate;
|
||||
return settings->uints.video_shader_subframes;
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
float refresh;
|
||||
/* An output without 120 Hz: no picture held for two refreshes, paced at 59.94 */
|
||||
assert(plan(59940, 59940, 120, 58.944, &refresh) == 1 && refresh < 60.0f);
|
||||
assert(plan(59940, 59940, 120, 60.0, &refresh) == 1 && refresh < 60.0f);
|
||||
assert(plan(59940, 59940, 60, 60.0, &refresh) == 1);
|
||||
/* ...and a 30 fps game still gets a frame between its frames, at the display's 60 */
|
||||
assert(plan(59940, 59940, 120, 30.0, &refresh) == 2 && fabsf(ps5_fg_target_fps - 59.94f) < 0.01f);
|
||||
/* VRR: 120 flips a second in the 59.94 Hz mode */
|
||||
assert(plan(59940, 119880, 120, 60.0, &refresh) == 2 && refresh > 119.0f && ps5_fg_dup == 1);
|
||||
assert(plan(59940, 119880, 60, 60.0, &refresh) == 2 && ps5_fg_dup == 2);
|
||||
/* Flips not timed: the mode is believed */
|
||||
assert(plan(59940, 0, 120, 60.0, &refresh) == 1 && refresh < 60.0f);
|
||||
/* The 119.88 Hz mode itself */
|
||||
assert(plan(119880, 0, 120, 60.0, &refresh) == 2 && ps5_fg_dup == 1);
|
||||
assert(plan(119880, 0, 60, 60.0, &refresh) == 2 && ps5_fg_dup == 2);
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / 'plan.c'
|
||||
path.write_text(source)
|
||||
exe = Path(tmp) / 'plan'
|
||||
subprocess.run(['cc', '-std=gnu99', str(path), '-o', str(exe), '-lm'], check=True)
|
||||
subprocess.run([str(exe)], check=True)
|
||||
@@ -4907,6 +4907,160 @@ EDITS += [('gfx/drivers/vulkan.c',
|
||||
' if (0 && fg_made_s > 1.1 * fg_emulated_s && fg_made_s < 8.0 / (double)ps5_fg_core_fps)\n',
|
||||
'patches/series, 0128: the producer')]
|
||||
|
||||
# Frame generation plans on the rate the output really shows flips at (patches/series, 0131). The mode list says 59.94 Hz
|
||||
# both where the console shows 120 flips a second (VRR, which the driver's idle vblank check reads as 48 Hz and turns its
|
||||
# 119.88 Hz mode off) and where it shows 60 (no 120 Hz on that output). 0121 assumed 119.88 for every 59.94 mode, so a 60 Hz
|
||||
# output held each picture for two refreshes and games ran at half speed with broken audio. The context now times flips
|
||||
# once, on its first swapchain, and the plan takes 119.88 only where they came that fast; a target above the display's
|
||||
# rate is the display's rate (docs/FINDINGS.md, 2026-10-17).
|
||||
EDITS += [('gfx/common/vulkan_common.c',
|
||||
'unsigned vulkan_ps5_display_refresh_x1000;\n'
|
||||
'\n'
|
||||
'static bool vulkan_create_display_surface(gfx_ctx_vulkan_data_t *vk,\n',
|
||||
'unsigned vulkan_ps5_display_refresh_x1000;\n'
|
||||
'/* patches/series, 0131: how fast the output shows flips, in millihertz, where\n'
|
||||
' * khr_display_ctx.c timed them (0: not timed). */\n'
|
||||
'unsigned vulkan_ps5_flip_rate_x1000;\n'
|
||||
'\n'
|
||||
'static bool vulkan_create_display_surface(gfx_ctx_vulkan_data_t *vk,\n',
|
||||
'patches/series, 0131: how fast the output shows flips, in millihertz, where'),
|
||||
('gfx/drivers_context/khr_display_ctx.c',
|
||||
'static bool gfx_ctx_khr_display_set_video_mode(void *data,\n',
|
||||
'/* Added by this port (patches/series, 0131): how fast the output shows flips, in\n'
|
||||
' * millihertz, timed on a new swapchain before RetroArch draws to it; 0 when a call\n'
|
||||
' * failed. Under FIFO a flip completes per refresh, so once every image is queued each\n'
|
||||
' * acquire returns as one completes, and the median of the intervals after that is the\n'
|
||||
' * output\'s period. The mode cannot say it: with VRR the console shows 120 flips a\n'
|
||||
' * second in its 59.94 Hz mode, and an output without 120 Hz shows 60 in the same mode. */\n'
|
||||
'static unsigned khr_display_ps5_flip_rate(gfx_ctx_vulkan_data_t *vk)\n'
|
||||
'{\n'
|
||||
' enum { TIMED = 9 };\n'
|
||||
' double at[VULKAN_MAX_SWAPCHAIN_IMAGES + 1 + TIMED];\n'
|
||||
' double gaps[TIMED];\n'
|
||||
' VkFenceCreateInfo fence_info;\n'
|
||||
' VkFence fence = VK_NULL_HANDLE;\n'
|
||||
' VkResult failed = VK_SUCCESS;\n'
|
||||
' unsigned presents = vk->context.num_swapchain_images + 1 + TIMED;\n'
|
||||
' unsigned i, j;\n'
|
||||
'\n'
|
||||
' if ( vk->swapchain == VK_NULL_HANDLE\n'
|
||||
' || vk->context.num_swapchain_images < 2\n'
|
||||
' || vk->context.num_swapchain_images > VULKAN_MAX_SWAPCHAIN_IMAGES\n'
|
||||
' || (vk->flags & VK_DATA_FLAG_EMULATING_MAILBOX))\n'
|
||||
' return 0;\n'
|
||||
' fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;\n'
|
||||
' fence_info.pNext = NULL;\n'
|
||||
' fence_info.flags = 0;\n'
|
||||
' if ((failed = vkCreateFence(vk->context.device, &fence_info, NULL, &fence)) != VK_SUCCESS)\n'
|
||||
' presents = 0;\n'
|
||||
' for (i = 0; i < presents && failed == VK_SUCCESS; i++)\n'
|
||||
' {\n'
|
||||
' struct timespec now;\n'
|
||||
' VkPresentInfoKHR present;\n'
|
||||
' VkResult queued;\n'
|
||||
' VkResult shown = VK_SUCCESS;\n'
|
||||
' uint32_t index = 0;\n'
|
||||
' failed = vkAcquireNextImageKHR(vk->context.device, vk->swapchain,\n'
|
||||
' UINT64_C(1000000000), VK_NULL_HANDLE, fence, &index);\n'
|
||||
' if (failed == VK_SUBOPTIMAL_KHR)\n'
|
||||
' failed = VK_SUCCESS;\n'
|
||||
' if (failed != VK_SUCCESS)\n'
|
||||
' break;\n'
|
||||
' failed = vkWaitForFences(vk->context.device, 1, &fence, VK_TRUE, UINT64_C(1000000000));\n'
|
||||
' vkResetFences(vk->context.device, 1, &fence);\n'
|
||||
' clock_gettime(CLOCK_MONOTONIC, &now);\n'
|
||||
' at[i] = (double)now.tv_sec * 1e3 + (double)now.tv_nsec / 1e6;\n'
|
||||
' /* the image goes back whatever the wait said: one kept would be missing from the frames that follow */\n'
|
||||
' present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;\n'
|
||||
' present.pNext = NULL;\n'
|
||||
' present.waitSemaphoreCount = 0;\n'
|
||||
' present.pWaitSemaphores = NULL;\n'
|
||||
' present.swapchainCount = 1;\n'
|
||||
' present.pSwapchains = &vk->swapchain;\n'
|
||||
' present.pImageIndices = &index;\n'
|
||||
' present.pResults = &shown;\n'
|
||||
'#ifdef HAVE_THREADS\n'
|
||||
' slock_lock(vk->context.queue_lock);\n'
|
||||
'#endif\n'
|
||||
' queued = vkQueuePresentKHR(vk->context.queue, &present);\n'
|
||||
'#ifdef HAVE_THREADS\n'
|
||||
' slock_unlock(vk->context.queue_lock);\n'
|
||||
'#endif\n'
|
||||
' if (failed == VK_SUCCESS && queued != VK_SUCCESS && queued != VK_SUBOPTIMAL_KHR)\n'
|
||||
' failed = queued;\n'
|
||||
' if (failed == VK_SUCCESS && shown != VK_SUCCESS && shown != VK_SUBOPTIMAL_KHR)\n'
|
||||
' failed = shown;\n'
|
||||
' }\n'
|
||||
' if (fence != VK_NULL_HANDLE)\n'
|
||||
' vkDestroyFence(vk->context.device, fence, NULL);\n'
|
||||
' if (failed != VK_SUCCESS)\n'
|
||||
' {\n'
|
||||
' RARCH_WARN("[PS5] Flips could not be timed (VkResult %d).\\n", (int)failed);\n'
|
||||
' return 0;\n'
|
||||
' }\n'
|
||||
' /* the intervals once every image was queued, sorted */\n'
|
||||
' for (i = 0; i < TIMED; i++)\n'
|
||||
' {\n'
|
||||
' const double gap = at[presents - TIMED + i] - at[presents - TIMED + i - 1];\n'
|
||||
' for (j = i; j > 0 && gaps[j - 1] > gap; j--)\n'
|
||||
' gaps[j] = gaps[j - 1];\n'
|
||||
' gaps[j] = gap;\n'
|
||||
' }\n'
|
||||
' RARCH_LOG("[PS5] Flips timed %.2f to %.2f ms apart, median %.2f ms.\\n",\n'
|
||||
' gaps[0], gaps[TIMED - 1], gaps[TIMED / 2]);\n'
|
||||
' return gaps[TIMED / 2] > 0.0 ? (unsigned)(1e6 / gaps[TIMED / 2] + 0.5) : 0;\n'
|
||||
'}\n'
|
||||
'\n'
|
||||
'static bool gfx_ctx_khr_display_set_video_mode(void *data,\n',
|
||||
'patches/series, 0131): how fast the output shows flips, in\n'),
|
||||
('gfx/drivers_context/khr_display_ctx.c',
|
||||
' settings->uints.video_swap_interval = 0;\n'
|
||||
' }\n'
|
||||
' }\n'
|
||||
'\n'
|
||||
' return true;\n',
|
||||
' settings->uints.video_swap_interval = 0;\n'
|
||||
' }\n'
|
||||
' }\n'
|
||||
' /* Added by this port (patches/series, 0131): where the mode says 60 Hz, how fast the\n'
|
||||
' * output really shows flips, timed once per process on its first swapchain. Frame\n'
|
||||
' * generation plans on it (retroarch.c). */\n'
|
||||
' {\n'
|
||||
' extern unsigned vulkan_ps5_flip_rate_x1000;\n'
|
||||
' static bool flips_timed;\n'
|
||||
' if (!flips_timed && khr->refresh_rate_x1000 < 100000)\n'
|
||||
' {\n'
|
||||
' flips_timed = true;\n'
|
||||
' vulkan_ps5_flip_rate_x1000 = khr_display_ps5_flip_rate(&khr->vk);\n'
|
||||
' RARCH_LOG("[PS5] The output shows %.3f flips a second in its %.3f Hz mode.\\n",\n'
|
||||
' vulkan_ps5_flip_rate_x1000 / 1000.0f, khr->refresh_rate_x1000 / 1000.0f);\n'
|
||||
' }\n'
|
||||
' }\n'
|
||||
'\n'
|
||||
' return true;\n',
|
||||
'patches/series, 0131): where the mode says 60 Hz, how fast the'),
|
||||
('retroarch.c',
|
||||
' /* The title declares high-frame-rate output (sce_sys/param.json attribute3 0x80040): the console sends\n'
|
||||
' * 119.88 Hz even where the Vulkan mode list shows its 3840x2160 mode at 59.94, and RetroArch must pace\n'
|
||||
' * on the real rate or its vsync and audio timing are those of half the display. */\n'
|
||||
' if (display < 100.0f)\n',
|
||||
' /* The title declares high-frame-rate output (sce_sys/param.json attribute3 0x80040): with VRR the console\n'
|
||||
' * shows 119.88 flips a second where the Vulkan mode list says 59.94, and RetroArch must pace on the real\n'
|
||||
' * rate or its vsync and audio timing are those of half the display.\n'
|
||||
' * patches/series, 0131: only where the flips were timed that fast (khr_display_ctx.c). An output without\n'
|
||||
' * 120 Hz shows 60, and planning 119.88 there held every picture for two refreshes: half speed. */\n'
|
||||
' extern unsigned vulkan_ps5_flip_rate_x1000;\n'
|
||||
' if (display < 100.0f && vulkan_ps5_flip_rate_x1000 > 100000)\n',
|
||||
'patches/series, 0131: only where the flips were timed that fast'),
|
||||
('retroarch.c',
|
||||
' const float target = settings->uints.video_framegen_target == 60 ? 59.94f : 119.88f;\n'
|
||||
' unsigned dup = (unsigned)(display / target + 0.5f);\n',
|
||||
' const float wanted = settings->uints.video_framegen_target == 60 ? 59.94f : 119.88f;\n'
|
||||
' /* patches/series, 0131: no more pictures than the display shows (120 on a 60 Hz output is 60) */\n'
|
||||
' const float target = wanted > display * 1.05f ? display : wanted;\n'
|
||||
' unsigned dup = (unsigned)(display / target + 0.5f);\n',
|
||||
'patches/series, 0131: no more pictures than the display shows')]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 2:
|
||||
|
||||
Reference in new issue
Block a user