mirror of
https://github.com/libretro/RetroArch
synced 2025-01-31 15:32:59 +00:00
325 lines
13 KiB
Python
325 lines
13 KiB
Python
|
import argparse
|
||
|
import subprocess
|
||
|
import sys
|
||
|
import time
|
||
|
import os
|
||
|
import logging
|
||
|
import platform
|
||
|
|
||
|
|
||
|
CTR_modifier = 1<<7
|
||
|
|
||
|
class mode:
|
||
|
def __init__(self, w:int = 640, h:int = 480, rr:float = 60):
|
||
|
self.width = int(w)
|
||
|
self.height = int(h)
|
||
|
self.refresh_rate = float(rr)
|
||
|
|
||
|
def __str__(self):
|
||
|
return "{}x{}@{}".format(self.width, self.height, self.refresh_rate)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "Mode: {}".format(str(self))
|
||
|
|
||
|
|
||
|
class geometry:
|
||
|
def __init__(self, h_size:float = 1.0, h_shift:int = 0, v_shift:int = 0):
|
||
|
self.h_size = float(h_size)
|
||
|
self.h_shift = int(h_shift)
|
||
|
self.v_shift = int(v_shift)
|
||
|
|
||
|
def __str__(self):
|
||
|
return "{}:{}:{}".format(self.h_size, self.h_shift, self.v_shift)
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
return self.h_size == other.h_size and self.h_shift == other.h_shift and self.v_shift == other.v_shift
|
||
|
|
||
|
def set_geometry(self, h_size:float, h_shift:int, v_shift:int):
|
||
|
self.h_size = float(h_size)
|
||
|
self.h_shift = int(h_shift)
|
||
|
self.v_shift = int(v_shift)
|
||
|
|
||
|
@classmethod
|
||
|
def set_from_string(cls, geom:str):
|
||
|
"""
|
||
|
geom should be in the form of 1.0:-5:2
|
||
|
"""
|
||
|
hsize, hshift, vshift = geom.split(':')
|
||
|
return cls(hsize, hshift, vshift)
|
||
|
|
||
|
def inc_hsize(self, step = 0.01, factor:int = 1): self.h_size += step * factor
|
||
|
def inc_hshift(self, step = 1, factor:int = 1): self.h_shift += step * factor
|
||
|
def inc_vshift(self, step = 1, factor:int = 1): self.v_shift += step * factor
|
||
|
def dec_hsize(self, step = 0.01, factor:int = 1): self.h_size -= step * factor
|
||
|
def dec_hshift(self, step = 1, factor:int = 1): self.h_shift -= step * factor
|
||
|
def dec_vshift(self, step = 1, factor:int = 1): self.v_shift -= step * factor
|
||
|
|
||
|
|
||
|
class crt_range:
|
||
|
def __init__(self, HfreqMin:float = 0.0, HfreqMax:float = 0.0, VfreqMin:float = 0.0, VfreqMax:float = 0.0, HFrontPorch:float = 0.0, HSyncPulse:float = 0.0, HBackPorch:float = 0.0, VFrontPorch:float = 0.0, VSyncPulse:float = 0.0, VBackPorch:float = 0.0, HSyncPol:int = 0, VSyncPol:int = 0, ProgressiveLinesMin:int = 0, ProgressiveLinesMax:int = 0, InterlacedLinesMin:int = 0, InterlacedLinesMax:int = 0):
|
||
|
self.HfreqMin = float(HfreqMin)
|
||
|
self.HfreqMax = float(HfreqMax)
|
||
|
self.VfreqMin = float(VfreqMin)
|
||
|
self.VfreqMax = float(VfreqMax)
|
||
|
self.HFrontPorch = float(HFrontPorch)
|
||
|
self.HSyncPulse = float(HSyncPulse)
|
||
|
self.HBackPorch = float(HBackPorch)
|
||
|
self.VFrontPorch = float(VFrontPorch)
|
||
|
self.VSyncPulse = float(VSyncPulse)
|
||
|
self.VBackPorch = float(VBackPorch)
|
||
|
self.HSyncPol = int(HSyncPol)
|
||
|
self.VSyncPol = int(VSyncPol)
|
||
|
self.ProgressiveLinesMin = int(ProgressiveLinesMin)
|
||
|
self.ProgressiveLinesMax = int(ProgressiveLinesMax)
|
||
|
self.InterlacedLinesMin = int(InterlacedLinesMin)
|
||
|
self.InterlacedLinesMax = int(InterlacedLinesMax)
|
||
|
|
||
|
@classmethod
|
||
|
def set_from_string(cls, range:str):
|
||
|
"""
|
||
|
range is in the form of 15625.00-16200.00,49.50-65.00,2.000,4.700,8.000,0.064,0.192,1.024,0,0,192,288,448,576
|
||
|
"""
|
||
|
HfreqRange, VfregRange, HFrontPorch, HSyncPulse, HBackPorch, VFrontPorch, VSyncPulse, VBackPorch, HSyncPol, VSyncPol, ProgressiveLinesMin, ProgressiveLinesMax, InterlacedLinesMin, InterlacedLinesMax = range.split(',')
|
||
|
HfreqMin, HfreqMax = HfreqRange.split('-')
|
||
|
VfreqMin, VfreqMax = VfregRange.split('-')
|
||
|
return cls(HfreqMin, HfreqMax, VfreqMin, VfreqMax, HFrontPorch, HSyncPulse, HBackPorch, VFrontPorch, VSyncPulse, VBackPorch, HSyncPol, VSyncPol, ProgressiveLinesMin, ProgressiveLinesMax, InterlacedLinesMin, InterlacedLinesMax)
|
||
|
|
||
|
def new_geometry_from_string(self, adjusted_geometry: str):
|
||
|
"""
|
||
|
range is in the shape of the switchres output: "H: 2.004, 4.696, 8.015 V: 0.447, 0.383, 2.425"
|
||
|
"""
|
||
|
#hfp, self.HSyncPulse, self.HBackPorch, vfp, self.VSyncPulse, self.VBackPorch = adjusted_geometry.split(', ')
|
||
|
hfp, self.HSyncPulse, hbp_and_vfp, self.VSyncPulse, self.VBackPorch = adjusted_geometry.split(', ')
|
||
|
self.HFrontPorch = hfp[3:]
|
||
|
self.HBackPorch, _, self.VFrontPorch = hbp_and_vfp.split(' ')
|
||
|
|
||
|
def __str__(self):
|
||
|
return "{}-{},{}-{},{},{},{},{},{},{},{},{},{},{},{},{}".format(
|
||
|
self.HfreqMin, self.HfreqMax, self.VfreqMin, self.VfreqMax, self.HFrontPorch, self.HSyncPulse, self.HBackPorch, self.VFrontPorch, self.VSyncPulse, self.VBackPorch, self.HSyncPol, self.VSyncPol, self.ProgressiveLinesMin, self.ProgressiveLinesMax, self.InterlacedLinesMin, self.InterlacedLinesMax)
|
||
|
|
||
|
|
||
|
def switchres_output_get_monitor_range(output:str):
|
||
|
for l in output.splitlines():
|
||
|
# The line to parse looks like:
|
||
|
# Switchres: Monitor range 15625.00-16200.00,49.50-65.00,2.000,4.700,8.000,0.064,0.192,1.024,0,0,192,288,448,576
|
||
|
if l[0:25] != "Switchres: Monitor range " : continue
|
||
|
logging.debug("Found! -> {}".format(l[25:]))
|
||
|
logging.debug(crt_range().set_from_string(l[25:]))
|
||
|
return crt_range().set_from_string(l[25:])
|
||
|
logging.warning("Couldn't find the monitor range!")
|
||
|
return None
|
||
|
|
||
|
def switchres_output_get_adjusted_crt_geometry(output:str):
|
||
|
for l in output.splitlines():
|
||
|
# The line to parse looks like:
|
||
|
# Adjusted geometry (1.000:0:0) H: 2.004, 4.696, 8.015 V: 0.223, 0.191, 1.212
|
||
|
# We need what is behind H:
|
||
|
if l[0:19] != "Adjusted geometry (" : continue
|
||
|
Hpos = l.find('H: ')
|
||
|
logging.debug("Found! -> {}".format(l[Hpos:]))
|
||
|
return l[Hpos:]
|
||
|
logging.warning("Couldn't find the adjusted crt geometry!")
|
||
|
return None
|
||
|
|
||
|
def switchres_output_get_adjusted_geometry(output:str):
|
||
|
for l in output.splitlines():
|
||
|
# The line to parse looks like:
|
||
|
# Adjusted geometry (1.000:0:0) H: 2.004, 4.696, 8.015 V: 0.223, 0.191, 1.212
|
||
|
# We need what is between parenthesis
|
||
|
if l[0:19] != "Adjusted geometry (" : continue
|
||
|
Hpos = l.find('H: ')
|
||
|
|
||
|
logging.debug("Found! -> {}".format(l[Hpos:]))
|
||
|
return l[19:Hpos - 2]
|
||
|
logging.warning("Couldn't find the adjusted geometry!")
|
||
|
return None
|
||
|
|
||
|
def switchres_output_get_command_exit_code(output:str):
|
||
|
for l in output.splitlines():
|
||
|
# The line to parse looks like:
|
||
|
# Process exited with value 256
|
||
|
if l[0:26] != "Process exited with value " : continue
|
||
|
logging.debug("Found! -> {}".format(l[26:]))
|
||
|
return int(l[26:])
|
||
|
logging.warning("Couldn't find the app exit code!")
|
||
|
return None
|
||
|
|
||
|
def launch_switchres(mode:mode, geom:geometry = geometry(), switchres_command:str = "switchres", launch_command:str = "grid", display:int = 0):
|
||
|
return_list = dict()
|
||
|
|
||
|
# The command line may not require launching a program, just to get the crt_range for example
|
||
|
cmd = [ switchres_command.split(" ")[0], str(mode.width), str(mode.height), str(mode.refresh_rate), '-v' ]
|
||
|
if switchres_command.split(" ")[1:]:
|
||
|
cmd.extend(switchres_command.split(" ")[1:])
|
||
|
if display > 0:
|
||
|
cmd.extend(['-d', str(display)])
|
||
|
if launch_command:
|
||
|
if display > 0:
|
||
|
launch_command += " {}".format(display)
|
||
|
cmd.extend(['-s', '-l', launch_command])
|
||
|
else:
|
||
|
cmd.extend(['-c'])
|
||
|
cmd.extend(['-g', str(geom)])
|
||
|
logging.debug("Calling: {} with text: {}".format(" ".join(cmd), os.getenv('GRID_TEXT')))
|
||
|
return_status = subprocess.run(cmd, capture_output=True, text=True)
|
||
|
logging.debug(return_status.stdout)
|
||
|
|
||
|
default_crt_range = switchres_output_get_monitor_range(return_status.stdout)
|
||
|
adjusted_geometry = switchres_output_get_adjusted_crt_geometry(return_status.stdout)
|
||
|
user_crt_range = default_crt_range
|
||
|
grid_return = None
|
||
|
|
||
|
if launch_command:
|
||
|
grid_return = switchres_output_get_command_exit_code(return_status.stdout)
|
||
|
user_crt_range.new_geometry_from_string(adjusted_geometry)
|
||
|
return_list['exit_code'] = grid_return
|
||
|
return_list['new_crt_range'] = user_crt_range
|
||
|
return_list['default_crt_range'] = default_crt_range
|
||
|
return_list['geometry'] = switchres_output_get_adjusted_geometry(return_status.stdout)
|
||
|
|
||
|
return return_list
|
||
|
|
||
|
def update_switchres_ini(range: crt_range, inifile:str = ''):
|
||
|
if not inifile:
|
||
|
sys.exit(0)
|
||
|
logging.info("Updating {} with crt_range {} (NOT YET IMPLEMENTED)".format(inifile, str(range)))
|
||
|
|
||
|
def readjust_geometry(geom: geometry, range:crt_range, return_code:int):
|
||
|
wanted_factor = 10 if return_code & CTR_modifier else 1
|
||
|
factor_message = " with CTRL pressed" if return_code & CTR_modifier else ''
|
||
|
# Disable the modifier
|
||
|
return_code = return_code & ~CTR_modifier
|
||
|
# This syntax requires python >= 3.10
|
||
|
match return_code:
|
||
|
# Pressed PAGEUP
|
||
|
case 68:
|
||
|
geom.inc_hsize(factor = wanted_factor)
|
||
|
# Pressed PAGEDOWN
|
||
|
case 69:
|
||
|
geom.dec_hsize(factor = wanted_factor)
|
||
|
# Pressed LEFT
|
||
|
case 64:
|
||
|
geom.dec_hshift(factor = wanted_factor)
|
||
|
# Pressed RIGHT
|
||
|
case 65:
|
||
|
geom.inc_hshift(factor = wanted_factor)
|
||
|
# Pressed DOWN
|
||
|
case 67:
|
||
|
geom.inc_vshift(factor = wanted_factor)
|
||
|
# Pressed UP
|
||
|
case 66:
|
||
|
geom.dec_vshift(factor = wanted_factor)
|
||
|
# Pressed ESCAPE / Q
|
||
|
case 1:
|
||
|
logging.info("Aborted!")
|
||
|
sys.exit(1)
|
||
|
# Pressed ENTER / RETURN
|
||
|
case 0:
|
||
|
logging.info("Finished!")
|
||
|
logging.info("Final geometry: {}".format(str(geom)))
|
||
|
logging.info("Final crt_range: {}".format(str(range)))
|
||
|
#update_switchres_ini(range, switchres_ini)
|
||
|
sys.exit(0)
|
||
|
# Pressed DEL / BACKSPACE
|
||
|
case 2:
|
||
|
geom = geometry(1.0, 0, 0)
|
||
|
# Pressed R
|
||
|
case 3:
|
||
|
logging.debug("Refreshing with the same geometry values if your screen was scrolling")
|
||
|
logging.debug("Readjusted geometry: {}".format(str(geom)))
|
||
|
return geom
|
||
|
|
||
|
def set_grid_text(top_txt:str = '', bottom_txt:str = '', geom:geometry = geometry()):
|
||
|
help_txt = """H size: {}
|
||
|
H shift: {}
|
||
|
V shift: {}
|
||
|
|
||
|
Arrows: shift screen - Page Up/Down: H size
|
||
|
ENTER: validate - ESC: cancel - DEL: reinit - R: reload
|
||
|
CTRL+key: step x10""".format(geom.h_size, geom.h_shift, geom.v_shift)
|
||
|
os.environ['GRID_TEXT'] = "\n \n{}".format("\n \n".join(filter(None, [top_txt, help_txt, bottom_txt])))
|
||
|
logging.debug(os.getenv('GRID_TEXT'))
|
||
|
|
||
|
def switchres_geometry_loop(mode: mode, switchres_command:str = "switchres", launch_command:str = "grid", display_nr:int = 0, geom:geometry = geometry()):
|
||
|
working_geometry = geom
|
||
|
top_txt = ''
|
||
|
|
||
|
while True:
|
||
|
# This launch is to confirm the geometry
|
||
|
sr_launch_return = launch_switchres(mode, working_geometry, switchres_command, launch_command = "", display = display_nr)
|
||
|
ret_geom = geometry.set_from_string(sr_launch_return['geometry'])
|
||
|
if ret_geom != working_geometry:
|
||
|
top_txt = "Geometry readjusted, was out of CRT range bounds"
|
||
|
logging.info("Warning: you've reached a crt_range limit, can't go further in the last direction. Setting back to {}".format(str(ret_geom)))
|
||
|
working_geometry = ret_geom
|
||
|
os.environ['GRID_TEXT'] = "\n".join([os.getenv('GRID_TEXT') or "", "({})".format(str(geom))])
|
||
|
set_grid_text(top_txt, '', working_geometry)
|
||
|
# Now is the real launch with the grid
|
||
|
sr_launch_return = launch_switchres(mode, working_geometry, switchres_command, launch_command, display_nr)
|
||
|
grid_return_code = sr_launch_return['exit_code']
|
||
|
sr_geometry = geometry.set_from_string(sr_launch_return['geometry'])
|
||
|
working_geometry = readjust_geometry(sr_geometry, sr_launch_return['new_crt_range'], grid_return_code)
|
||
|
os.environ['GRID_TEXT'] = ""
|
||
|
top_txt = ''
|
||
|
time.sleep(2)
|
||
|
|
||
|
|
||
|
#
|
||
|
# MAIN
|
||
|
#
|
||
|
|
||
|
# The default switchres.ini file depends on the platform
|
||
|
if platform.system() == 'Linux':
|
||
|
default_switchres_ini = '/etc/switchres.ini'
|
||
|
else:
|
||
|
default_switchres_ini = 'switchres.ini'
|
||
|
|
||
|
parser = argparse.ArgumentParser(description='Switchres wrapper to adjust a crt_range for switchres.ini')
|
||
|
parser.add_argument('mode', metavar='N', type=float, nargs=3,
|
||
|
help='width height refresh_rate')
|
||
|
parser.add_argument('-l', '--launch', metavar='launch', type=str, default='grid',
|
||
|
help='The program you want to launch')
|
||
|
# parser.add_argument('-i', '--ini', metavar='ini', type=str, default=default_switchres_ini,
|
||
|
# help='The switchres.ini file to edit')
|
||
|
parser.add_argument('-s', '--switchres', metavar='binary', type=str, default='switchres',
|
||
|
help='The switchres binary to use')
|
||
|
parser.add_argument('-d', '--display', metavar='display', type=int, default=0,
|
||
|
help='Set the display to calibrate')
|
||
|
#parser.add_argument('-m', '--monitor', metavar='monitor', type=str, default='arcade_15',
|
||
|
# help='The monitor preset base, to override the switchres.ini (NOT YET IMPLEMENTED)')
|
||
|
parser.add_argument('-g', '--geometry', metavar='geometry', type=str, default='1.0:0:0',
|
||
|
help='Start with a predefined geometry')
|
||
|
parser.add_argument('-v', '--verbose', action='count', default=0, help='Verbose mode')
|
||
|
|
||
|
|
||
|
if sys.version_info.major < 3:
|
||
|
raise Exception("Must use Python 3.7 or later")
|
||
|
if sys.version_info.minor < 10:
|
||
|
raise Exception("Must use Python 3.7 or later")
|
||
|
|
||
|
#args = parser.parse_args(['320', '240', '59.94'])
|
||
|
args = parser.parse_args()
|
||
|
|
||
|
# Set log level according to wanted verbosity
|
||
|
loggingLevel = logging.INFO
|
||
|
logging.basicConfig(stream=sys.stdout, level=loggingLevel, format='%(message)s')
|
||
|
if args.verbose > 0:
|
||
|
loggingLevel = logging.DEBUG
|
||
|
logger = logging.getLogger()
|
||
|
logger.setLevel(loggingLevel)
|
||
|
for handler in logger.handlers:
|
||
|
handler.setLevel(loggingLevel)
|
||
|
for handler in logging.root.handlers[:]:
|
||
|
logging.root.removeHandler(handler)
|
||
|
logging.basicConfig(stream=sys.stdout, level=loggingLevel,
|
||
|
format='[%(levelname)s] %(filename)s/%(funcName)s(%(lineno)d): %(message)s')
|
||
|
logging.debug("Specified logging level: {}".format(args.verbose))
|
||
|
logging.debug("Command line arguments: {}".format(args))
|
||
|
|
||
|
logging.debug(args)
|
||
|
|
||
|
command_mode = mode(args.mode[0], args.mode[1], args.mode[2])
|
||
|
geometry_arg = geometry.set_from_string(args.geometry)
|
||
|
|
||
|
switchres_geometry_loop(command_mode, args.switchres, args.launch, args.display, geometry_arg)
|