33from __future__ import annotations
44
55import argparse
6+ import base64
67import json
8+ import logging
79import os
10+ import subprocess
811import sys
912import time
1013
2124else :
2225 APP_DIR = os .path .dirname (os .path .dirname (os .path .dirname (os .path .abspath (__file__ ))))
2326BACKUP_DIR = os .path .join (APP_DIR , "backups" )
27+ PROFILES_DIR = os .path .join (APP_DIR , "profiles" )
2428VF_CTRL_SIZE = vf_points .VF_CTRL_VER
2529
2630
2731def _api (gpu_index : int = 0 ) -> NvApi :
2832 return NvApi (gpu_index = gpu_index )
2933
3034
31- def _ensure_supported (force : bool = False ) -> bool :
32- if force :
33- print ("WARNING: --force-driver set, skipping driver version check." , file = sys .stderr )
34- print ("WARNING: This is dangerous if the NvAPI layout changed." , file = sys .stderr )
35- return True
36- ok , msg = driver_check .ensure_supported_driver ()
37- if not ok :
38- print (f"ERROR: { msg } " , file = sys .stderr )
39- return False
40- ok , msg = driver_check .ensure_supported_gpu ()
41- if not ok :
42- print (f"ERROR: { msg } " , file = sys .stderr )
43- return False
44- return True
45-
46-
4735def _ensure_supported_auto (api : NvApi , args ) -> bool :
4836 """Like _ensure_supported, but automatically tries crack for unknown drivers."""
4937 if getattr (args , "force_driver" , False ):
@@ -131,6 +119,21 @@ def _safe_restore(restore_func, label: str, original_exc: Exception | None = Non
131119
132120_last_write_time = 0.0
133121WRITE_COOLDOWN_SECONDS = 1.0
122+ LOG = logging .getLogger ("xbar5090" )
123+
124+
125+ def _setup_logging (args ) -> None :
126+ level = logging .WARNING
127+ if getattr (args , "verbose" , False ):
128+ level = logging .DEBUG
129+ elif getattr (args , "quiet" , False ):
130+ level = logging .ERROR
131+ handlers = [logging .StreamHandler ()]
132+ log_file = getattr (args , "log_file" , None )
133+ if log_file :
134+ handlers .append (logging .FileHandler (log_file , encoding = "utf-8" ))
135+ logging .basicConfig (level = level , handlers = handlers ,
136+ format = "%(asctime)s %(levelname)s %(message)s" )
134137
135138
136139def _write_cooldown () -> None :
@@ -565,15 +568,142 @@ def cmd_restore_snapshot(args, api: NvApi) -> int:
565568 return 0
566569
567570
571+ def _profile_path (name : str ) -> str :
572+ return os .path .join (PROFILES_DIR , f"{ name } .json" )
573+
574+
575+ def cmd_profile_save (args , api : NvApi ) -> int :
576+ name = args .name
577+ clk_buf , _ , _ = clk_domains .read_clock_domains (api )
578+ prop_buf , _ = prop_rels .read_prop_rels (api )
579+ active = vf_points .active_mask (api )
580+ vf_buf = vf_points .get_control (api , active )
581+ os .makedirs (PROFILES_DIR , exist_ok = True )
582+ path = _profile_path (name )
583+ payload = {
584+ "created" : time .strftime ("%Y-%m-%d %H:%M:%S" ),
585+ "clk_b64" : base64 .b64encode (buf_to_bytes (clk_buf )).decode (),
586+ "prop_b64" : base64 .b64encode (buf_to_bytes (prop_buf )).decode (),
587+ "vf_b64" : base64 .b64encode (buf_to_bytes (vf_buf )).decode (),
588+ "gpu_index" : api .gpu_index ,
589+ }
590+ with open (path , "w" , encoding = "utf-8" ) as f :
591+ json .dump (payload , f , ensure_ascii = False , indent = 2 )
592+ print (f"Profile saved: { path } " )
593+ return 0
594+
595+
596+ def cmd_profile_apply (args , api : NvApi ) -> int :
597+ if not is_admin ():
598+ print ("ERROR: profile apply requires administrator." , file = sys .stderr )
599+ return 2
600+ if not _ensure_supported_auto (api , args ):
601+ return 2
602+ path = _profile_path (args .name )
603+ if not os .path .isfile (path ):
604+ print (f"ERROR: profile not found: { path } " , file = sys .stderr )
605+ return 2
606+ snap = backup_mod .load_snapshot (path )
607+ clk_buf = bytes_to_buf (snap ["clk_bytes" ], clk_domains .CLK_DOMAINS_BUFSIZE )
608+ prop_buf = bytes_to_buf (snap ["prop_bytes" ], prop_rels .PROP_RELS_BUFSIZE )
609+ vf_buf = bytes_to_buf (snap ["vf_bytes" ], vf_points .VF_CTRL_VER )
610+
611+ cur_clk , _ , _ = clk_domains .read_clock_domains (api )
612+ cur_prop , _ = prop_rels .read_prop_rels (api )
613+ cur_vf = vf_points .get_control (api , vf_points .active_mask (api ))
614+ backup_mod .save_snapshot (BACKUP_DIR , "pre_profile_apply" , cur_clk , cur_prop , cur_vf ,
615+ {"gpu_index" : api .gpu_index })
616+
617+ try :
618+ clk_domains .restore_from_buf (api , clk_buf )
619+ prop_rels .restore_from_buf (api , prop_buf )
620+ vf_points .set_control (api , vf_buf )
621+ except Exception as e :
622+ _safe_restore (lambda : clk_domains .restore_from_buf (api , cur_clk ), "clk_domains" , e )
623+ _safe_restore (lambda : prop_rels .restore_from_buf (api , cur_prop ), "prop_rels" , e )
624+ _safe_restore (lambda : vf_points .set_control (api , cur_vf ), "vf_control" , e )
625+ print (f"ERROR: profile apply failed, rolled back: { e } " , file = sys .stderr )
626+ return 3
627+
628+ _ , f , m = clk_domains .read_clock_domains (api )
629+ _ , r = prop_rels .read_prop_rels (api )
630+ print (f"Profile applied: { args .name } (XBAR={ f } kHz, MSVDD={ m } uV, ratio_raw={ r } )" )
631+ return 0
632+
633+
634+ def cmd_profile_list (args , api : NvApi ) -> int :
635+ if not os .path .isdir (PROFILES_DIR ):
636+ print ("No profiles saved." )
637+ return 0
638+ for name in sorted (os .listdir (PROFILES_DIR )):
639+ if name .endswith (".json" ):
640+ print (name [:- 5 ])
641+ return 0
642+
643+
568644def cmd_perf (args , api : NvApi ) -> int :
569645 buf = perf_limits .get_perf_limits (api )
570- for uid in (perf_limits .XBAR_MAX_USER_ID , perf_limits .XBAR_MIN_USER_ID ):
571- base = perf_limits .find_entry_by_user_id (buf , uid )
572- if base is None :
573- print (f"user_id { uid :#x} : not found" )
574- continue
575- vals = [get_u32 (buf , base + o ) for o in range (0 , 0x40 , 4 )]
576- print (f"user_id { uid :#x} base={ base :#x} first8={ [hex (v ) for v in vals [:8 ]]} " )
646+ entries = perf_limits .parse_entries (buf )
647+ if getattr (args , "json" , False ):
648+ print (json .dumps ({"entries" : entries }, indent = 2 ))
649+ return 0
650+ for e in entries :
651+ print (f"user_id { e ['user_id' ]:#x} base={ e ['base' ]:#x} first8={ [hex (v ) for v in e ['values' ][:8 ]]} " )
652+ return 0
653+
654+
655+ def _autostart_script_path () -> str :
656+ return os .path .join (APP_DIR , "autostart_xbar.ps1" )
657+
658+
659+ def _write_autostart_script () -> str :
660+ if getattr (sys , "frozen" , False ):
661+ cmd = f"& '{ sys .executable } '"
662+ else :
663+ py = sys .executable
664+ run = os .path .join (APP_DIR , "run.py" )
665+ cmd = f"& '{ py } ' '{ run } '"
666+ script = f"""$ErrorActionPreference = 'Continue'
667+ { cmd } vfp-auto-range --msvdd-mv 1150 --freq-khz 88000 --yes *> $env:TEMP\\ xbar5090_autostart.log 2>&1
668+ { cmd } set-xbar --freq-khz 205000 --msvdd-uv 10000 --yes *>> $env:TEMP\\ xbar5090_autostart.log 2>&1
669+ { cmd } set-ratio --ratio 1.2 --yes *>> $env:TEMP\\ xbar5090_autostart.log 2>&1
670+ """
671+ path = _autostart_script_path ()
672+ with open (path , "w" , encoding = "utf-8" ) as f :
673+ f .write (script )
674+ return path
675+
676+
677+ def cmd_autostart_install (args , api : NvApi ) -> int :
678+ if not is_admin ():
679+ print ("ERROR: autostart-install requires administrator." , file = sys .stderr )
680+ return 2
681+ script = _write_autostart_script ()
682+ task_name = "xbar5090 Autostart"
683+ cmd = (
684+ f'schtasks /Create /TN "{ task_name } " '
685+ f'/TR "powershell -NoProfile -ExecutionPolicy Bypass -File \\ "{ script } \\ "" '
686+ f'/SC ONLOGON /RL HIGHEST /F'
687+ )
688+ r = subprocess .run (cmd , shell = True , capture_output = True , text = True )
689+ if r .returncode != 0 :
690+ print (f"ERROR: failed to create scheduled task: { r .stderr .strip ()} " , file = sys .stderr )
691+ return 1
692+ print (f"Autostart installed: { task_name } " )
693+ print (f"Script: { script } " )
694+ return 0
695+
696+
697+ def cmd_autostart_remove (args , api : NvApi ) -> int :
698+ if not is_admin ():
699+ print ("ERROR: autostart-remove requires administrator." , file = sys .stderr )
700+ return 2
701+ task_name = "xbar5090 Autostart"
702+ r = subprocess .run (f'schtasks /Delete /TN "{ task_name } " /F' , shell = True , capture_output = True , text = True )
703+ if r .returncode != 0 :
704+ print (f"ERROR: failed to remove scheduled task: { r .stderr .strip ()} " , file = sys .stderr )
705+ return 1
706+ print (f"Autostart removed: { task_name } " )
577707 return 0
578708
579709
@@ -811,8 +941,11 @@ def cmd_l2_test(args, api: NvApi) -> int:
811941
812942
813943def main (argv = None ) -> int :
814- parser = argparse .ArgumentParser (prog = "xbar5090" , description = "RTX 5090 XBAR controls via private NvAPI" )
944+ parser = argparse .ArgumentParser (prog = "xbar5090" , description = "RTX 50-series XBAR controls via private NvAPI" )
815945 parser .add_argument ("--gpu" , type = int , default = 0 , help = "GPU index (default 0)" )
946+ parser .add_argument ("--verbose" , action = "store_true" , help = "enable debug logging" )
947+ parser .add_argument ("--quiet" , action = "store_true" , help = "only show errors" )
948+ parser .add_argument ("--log-file" , help = "write logs to this file" )
816949 sub = parser .add_subparsers (dest = "cmd" )
817950
818951 write_common = argparse .ArgumentParser (add_help = False )
@@ -849,7 +982,11 @@ def main(argv=None) -> int:
849982 p_rest = sub .add_parser ("vfp-restore" , parents = [write_common ])
850983 p_rest .add_argument ("--backup" , required = True )
851984 p_rest .set_defaults (func = cmd_vfp_restore )
852- sub .add_parser ("perf" ).set_defaults (func = cmd_perf )
985+ p_perf = sub .add_parser ("perf" )
986+ p_perf .add_argument ("--json" , action = "store_true" , help = "output JSON" )
987+ p_perf .set_defaults (func = cmd_perf )
988+ sub .add_parser ("autostart-install" ).set_defaults (func = cmd_autostart_install )
989+ sub .add_parser ("autostart-remove" ).set_defaults (func = cmd_autostart_remove )
853990 sub .add_parser ("doctor" ).set_defaults (func = cmd_doctor )
854991 sub .add_parser ("probe" , help = "auto-verify driver layout after a driver update (read-only)" ).set_defaults (func = cmd_probe )
855992 sub .add_parser ("crack" , help = "auto-match driver function IDs from candidates.json (read-only probing)" ).set_defaults (func = cmd_crack )
@@ -866,12 +1003,20 @@ def main(argv=None) -> int:
8661003 p_rs = sub .add_parser ("restore-snapshot" , parents = [write_common ], help = "restore clk+prop+vf snapshot" )
8671004 p_rs .add_argument ("--snapshot" , required = True )
8681005 p_rs .set_defaults (func = cmd_restore_snapshot )
1006+ p_ps = sub .add_parser ("profile-save" , help = "save current settings as a named profile" )
1007+ p_ps .add_argument ("name" )
1008+ p_ps .set_defaults (func = cmd_profile_save )
1009+ p_pa = sub .add_parser ("profile-apply" , parents = [write_common ], help = "apply a named profile" )
1010+ p_pa .add_argument ("name" )
1011+ p_pa .set_defaults (func = cmd_profile_apply )
1012+ sub .add_parser ("profile-list" , help = "list saved profiles" ).set_defaults (func = cmd_profile_list )
8691013
8701014 # If no subcommand is given, launch the interactive wizard directly.
8711015 # This makes double-click / "Run as administrator" on the exe work.
8721016 parser .set_defaults (func = cmd_wizard )
8731017
8741018 args = parser .parse_args (argv )
1019+ _setup_logging (args )
8751020 if getattr (args , "cmd" , None ) in {
8761021 "set-xbar" , "set-ratio" , "vfp-set-range" , "vfp-auto-range" ,
8771022 "vfp-restore" , "reset" , "restore-snapshot" , "wizard" ,
0 commit comments