Skip to content

Instantly share code, notes, and snippets.

@4shpool
Last active Dec 20, 2020
Embed
What would you like to do?

VPCCalc (lr-viewport calculator for MAME arcade roms)

Preface

In configuring and populating my DIY wall-mounted retrogaming controlpanel/monitor setup (Raspberry/RetroPie-based) I, over time, came to the conclusion, that whilst playing vertical games where just fine, for horizontal games I wasn't able to hold a "It's all in my View"-Focus on the image. Depending on the games pace and elements to be aware of, things farther away from my viewpoint/focus slipt my notice and I wasn't able to get/keep a steady view on the display in which my awareness captured the whole screen at once. As moving the display farther away from the controlpanel was not feasible, my thoughts went towards custom viewports for those and so ... Project VPC came to be. ...


Overview

VPCCalc.py will read a reference dat created via VPC_CreateDat.py (if none is provided via commandline, it will ask for one). Roms can be selected from it (one at a time (see postscriptum)) and for (single display) raster based ones and a given max. resolution/display-size, an integer scaled viewport for a DAR of 4/3[H] or 3/4[V] (in one dimension/for one reference axis) will be calculated. Also the scaling factor of each axis, for the max. framesize (DAR conform) within the given max. resolution, will be presented.

IMAGE

  • Rom Selection - select a rom from the provided database: Text Input and/or via the listbox.
    Supported keys for navigating the listbox: Up/Down, PgUp/PgDwn, Home/End
  • Rom Description - Column 'Desc' from the database (rom.description from mame.xml)
  • Display Information - Display Information from the database and derived base values for the selected rom
    (for screentype = 'raster'), see Aspect Ratios and Arcade roms and DAR below for further details.
  • Rom Information - Basic Information for the selected Rom:
    • Category - derived from the catver
    • Sampleset - Sampleset used by rom (if any)
    • CHD - associated CHD file (if any) (tooltip will be set to this, just in case the name becomes truncated in the UI)
    • Clone of - Parent Rom (if rom is clone)
    • Driver, Graphic, Sound - Driver Status, depending of the mame.xml generation used for the input:
      If no value for emulation and graphic is provided, Driver will list (see its tooltip) the status of Driver (Sound).
      If a value for emulation is provided, Driver will list the status of Driver (Emulation), and if a value is provided for graphic and/or sound those will be shown in the UI.
  • Options Set the targeted display resolution and select the reference axis (the one to be integer scaled).
    If available: select lower (int based) scaling factor for the reference axis; copy the (nScaled) viewport definition (lr_rom.cfg format) to the clipboard (see Clipboard: Viewport Example below)
  • Calculated Resolutions Information for the selected nScale factor (calculated max. as default upon rom selection) and the scaling factors for both axes utilizing the displays full resolution whilst honouring source DAR.
    As I don't see any benefit of allowing uneven pixelsizes for the rounding of fractional pixeldimensions, the calculated frames resolution is bound to an even number of pixels (the error for the DAR is, in both cases, negligible and even numbers are far easier to handle (Example: 224*4/4=298.667, nearest INT 299, nearest even INT 298)).
Aspect Ratios

From Wikipedia: Pixel Aspect Ratio: Introduction, Aspect Ratio (Image): Distinctions

DAR: Display Aspect Ratio; Aspect ratio of the image as displayed.
SAR: Storage Aspect Ratio; Ratio of the pixel dimensions the image is stored in.
PAR: Pixel Aspect Ratio; Aspect ratio of the pixels themselves.
-> Calculating PAR from DAR and SAR: PAR = DAR/SAR

Notes:

Arcade roms and DAR

Citing from the MAMEDev Wiki: LAY File Basics - Part I

 The screen ratio for most vertical arcade games is 3:4.
 The screen ratio for most horizontal games is 4:3.

Let me explain that now. Most arcade games through the 20th century and into the early 21st century ran on a standard arcade monitor, with a 4:3 ratio, just like old CRT monitors. Vertical games simply rotated the monitor 90 degrees. No matter what resolution an arcade PCB runs internally, it is expected to be stretched out to a standard 4:3 monitor. So if you want to calculate the screen ratio correctly:

 The width divided by the height on a horizontal game equals 1.33.  
 The width divided by the height on a vertical game equals 0.75.  

(Exceptions include games that supported wide-screen monitors. Sega Virtua Racing and Capcom Street Fighter III 2nd Impact: Giant Attack are examples of games that can be configured to use a 16:9 widescreen monitor.)

Clipboard: Viewport Example
#VPC-Viewport for rom: zaxxon
# 20 = Config, 21 = 1:1 PAR, 22 = Core Provided, 23 = Custom Viewport
aspect_ratio_index = "23"
# these two define the pixel size of the emulated screen
custom_viewport_width = "896"
custom_viewport_height = "1194"
# the following two decide how far from the left and top the game screen is shown
# (1600 - 896) / 2 =
custom_viewport_x = "352"
# (1200 - 1194) / 2 =
custom_viewport_y = "3"

Postscriptum

  • An unfiltered datfile created via VPC_CreateDat may contain many entries which are not arcade roms and though VPCalc is limited to calculate Viewports only for the screentypes raster and vector (later with only marginal usability for now), even there may remain many unwanted entries (depending on mame.xml used), and/or items where the calculations performed by VPCalc may simply be inapprobiate for. As my intention with this project was, besides beeing curious about the information provided within mames datfile.xml, to help me setting up just a few titles and so, within this context, the limitations ain't dimishing the scripts usefulness to me. It simply is important to keep in mind, that each calculation/title has to be checked/verified by the user (does it sound right (assumption of DAR={(4/3),(3/4)})?, may it contain various resolutions/is the resolution used for the calculation the one we want to be nScaled?, did we missed some form of multidisplay?, dunno?, whatsnot?, etc. (research))
  • The UI was developed by utilizing the package PySimpleGUI on Python(.org) 3.8 under Windows 10 (in late 2020). Under *nix Systems the UI Element [Button] seems to have some inner padding left/right added and that may be tinker related. I am so far unaware of a solution to get the behaviour for windows and *nix on par (haven't tried it on other systems so far).
    Next to the button-element problem, the font used by the os (the script requests courier as a monospaced font) may have a (deep) impact on the layout (from my trials: KDE under Ubuntu 18.04 is a nearly perfect match towards the windows UI (if only the mentioned button width could be dealt with...)), but under Ubuntu 20.04 (default desktop environment) the whole UI is a horizontal mess! As this was my first run in python with UIs and I decided to give PySimpleGUI a try, I am lucky/happy enough to got it somewhat appealing on my setup, and I am not seeing that I may solve the OS/fonts problems anytime soon or later...
# -*- coding: utf-8 -*-
#coded using python 3.8 (under Windows 10)
#ToDo:
# * Allow scaling of the viewport for vector based roms....
# .... maybe via a percentage slider, with some arbitrary minimum,
# .... or by an "InputText"-element for the reference axis pixel dimension .!?.
# * SRC to PAR=1 Calculations -> utilize choosen reference axis, don't stick to
# .... default one. &Xtra: avoid downscaling!
import sys
import os
import re
import textwrap
from fractions import Fraction
#import numpy as np //not needed so far
#& there may be "better" ways/libraries for this use-case, but for now I stick to pandas...
import pandas as pd
import mpmath as mp
import PySimpleGUI as sg
import clipboard as clip
#using mpmath - for mp.identify() and simply because I really, really hate the trouble with floats ....
#setting mpmaths precision to 100 decimals
mp.mp.dps=100
# ------------------ Setup GUI ------------------
def SetupUI(dbase, tx, ty):
#dbase pandas dataframe
#tx,ty width/height of the target frame
ltxtrcol=44 #length for the textelements of the UIs right column
sg.SetOptions(font=("Courier", 11))
sg.theme('Default1')
#The padding had no concept to start with and was added along the way, it is a mess that could need some rework/streamlining ("recognizable concept")...
# ... as is the whole sizing/padding in regards to fonts/OS used :/
tcol = [[sg.Text(text="*", key='-romdesc-', relief=sg.RELIEF_GROOVE,pad=(0, 0), size=(66, 2))]]
lcol1 = [[sg.InputText(key='-ROM_IN-', enable_events=False, pad=((1, 9), (3, 5)), size=(15, 1))], #cause of using window.keyboard_events, there is no need for one here
#populate a listbox with all the roms from the database and extend it to include a dummy NIL rom
[sg.Listbox(values=dbase["ROM"].tolist()+["<unlisted rom>"], select_mode=sg.LISTBOX_SELECT_MODE_SINGLE , key='-LBOX-', enable_events=True, pad=((1, 1), 3), size=(15,8))]]
lcol2 = [[sg.Button(button_text=" - Set Target - ", key="-TARGET-", pad=(5, 1), size=(16, 1))],
[sg.Text(text=" nScale x ", pad=(5, 1)), sg.Combo(values=[0], default_value=0, key="-nScale-", readonly=True, enable_events=True, pad=((0,1), 1), size=(3, 1))]]
rcol1 = [[sg.Text(text="*", key='-screen-', pad=(9, (0, 0)), size=(ltxtrcol, 1))],
[sg.Text(text="*", key='-DAR_text-', pad=(9,(0, 2)), size=(ltxtrcol, 1))],
[sg.Text(text="*", key='-SAR_xy-', pad=(9, (3, 1)), size=(ltxtrcol, 1))],
[sg.Text(text="*", key='-SAR_ratio-', pad=(9,0), size=(ltxtrcol, 1))],
[sg.Text(text="*",key='-SAR_val-', pad=(9,(0,2)),size=(ltxtrcol, 1))],
[sg.Text(text="*", key='-PAR_ratio-', pad=(9,(2,0)), size=(ltxtrcol, 1))],
[sg.Text(text="*",key='-PAR_val-', pad=(9,0), size=(ltxtrcol, 1))],
[sg.Text(text="*", key='-SimpleSize-', pad=(9, (6, 1)), size=(ltxtrcol, 1))]]
rcol2=[[sg.Text(text="Targeted Display/Frame: "+tx+" x "+ty, key="-TARGETED-", pad=((5, 0), (2,1)), size=(ltxtrcol-5, 1))],
[sg.Text(text="Axis for integer scaling:", pad=((4, 0), 1), size=(25, 1)),
sg.InputCombo(values=(' horizontal', ' vertical'), default_value=' vertical', key='-AXIS-', readonly=True, enable_events=True, pad=(2, 1), size=(12, 1))]]
rcol2b=[[sg.Text(text=" ", pad=(0, 0), size=(1, 2)), sg.Button(button_text="copy\nCVP", key="-CVP-", button_color=('#000000','#c0c0c0'), disabled=True, pad=(0, (3,0)), size=(4, 2))]]
mcol1 = [[sg.Column(lcol1, vertical_alignment='top'), sg.Column(rcol1, vertical_alignment='top')]]
mcol2_addextra=False
if dbase["emulation"][1]=="unknown":
if dbase["graphic"][1]=="unknown":
#oldest xml variant I encountered, sound should be present
DRVTTip=" Driver (Sound) Status "
DRVStatus=0
else:
DRVTTip=" Driver Status "
DRVStatus=1
mcol2_addextra=True
else:
DRVTTip=" Driver (Emulation) Status "
if dbase["graphic"][1]!="unknown" or dbase["sound"][1]!="unknown":
mcol2_addextra=True
DRVStatus=2
else:
DRVStatus=3
if mcol2_addextra:
mcol2 = [[sg.Text(text="Category :", font=("Courier", 10), pad=((5, 0),(3, 1)), size=(11, 1)), sg.Text(text="NIL", key='-CAT-', font=("Courier", 10), pad=((0, 6), (3, 1)), size=(61, 1))],
[sg.Text(text="Sampleset :", font=("Courier", 10), pad=((5, 0),1), size=(11, 1)), sg.Text(text="-", key='-SAMPLE-', font=("Courier", 10), pad=((0, 1), 1), size=(18, 1)),
sg.Text(text="CHD... :", font=("Courier", 10), pad=((5, 0),1), size=(8, 1)), sg.Text(text="------", key='-CHD-', tooltip="-", font=("Courier", 10), pad=(0, 1), size=(34, 1))],
[sg.Text(text="Clone of :", font=("Courier", 10), pad=((5, 0),(1, 2)), size=(11, 1)), sg.Text(text="-", key='-CLONE-', font=("Courier", 10), pad=((0, 1), (1, 2)), size=(18, 1)),
sg.Text(text="Driver :", tooltip=DRVTTip, font=("Courier", 10), pad=((5, 0),1), size=(8, 1)), sg.Text(text="- (-)", key='-DRIVER-', tooltip=DRVTTip, font=("Courier", 10), pad=(0, 1), size=(34, 1))],
[sg.Text(text=" Graphic :", font=("Courier", 10), pad=((5, 0),(1, 2)), size=(11, 1)), sg.Text(text="-", key='-GFX-', font=("Courier", 10), pad=((0, 1), (1, 2)), size=(18, 1)),
sg.Text(text="Sound :", font=("Courier", 10), pad=((5, 0),1), size=(8, 1)), sg.Text(text="-", key='-SND-', font=("Courier", 10), pad=(0, 1), size=(18, 1))]]
else:
mcol2 = [[sg.Text(text="Category :", font=("Courier", 10), pad=((5, 0),(3, 1)), size=(11, 1)), sg.Text(text="NIL", key='-CAT-', font=("Courier", 10), pad=((0, 6), (3, 1)), size=(61, 1))],
[sg.Text(text="Sampleset :", font=("Courier", 10), pad=((5, 0),1), size=(11, 1)), sg.Text(text="-", key='-SAMPLE-', font=("Courier", 10), pad=((0, 1), 1), size=(18, 1)),
sg.Text(text="CHD... :", font=("Courier", 10), pad=((5, 0),1), size=(8, 1)), sg.Text(text="- (-)", key='-CHD-', tooltip="-", font=("Courier", 10), pad=(0, 1), size=(34, 1))],
[sg.Text(text="Clone of :", font=("Courier", 10), pad=((5, 0),(1, 2)), size=(11, 1)), sg.Text(text="-", key='-CLONE-', font=("Courier", 10), pad=((0, 1), (1, 2)), size=(18, 1)),
sg.Text(text="Driver :", tooltip=DRVTTip, font=("Courier", 10), pad=((5, 0),1), size=(8, 1)), sg.Text(text="- (-)", key='-DRIVER-', tooltip=DRVTTip, font=("Courier", 10), pad=(0, 1), size=(34, 1))]]
lcol3 = [[sg.Text(text="*", key="-CALC_XY-", pad=(5, 3), size=(32, 1))],
[sg.Text(text="*", key="-SCALE_X-", pad=(5, 0), size=(32, 1))],
[sg.Text(text="*", key="-SCALE_Y-", pad=(5, (1, 3)), size=(32, 1))]]
rcol3 = [[sg.Text(text="*", key="-DCALC_XY-", pad=(0, 3), size=(32, 1))],
[sg.Text(text="*", key="-DSCALE_X-", pad=(0, 0), size=(32, 1))],
[sg.Text(text="*", key="-DSCALE_Y-", pad=(0, (1, 3)), size=(32, 1))]]
layout = [[sg.Column(tcol, vertical_alignment='top')],
[sg.Frame('', mcol1, relief=sg.RELIEF_RIDGE, vertical_alignment='top', pad=(5, (1, 1)))],
[sg.Frame('', mcol2, key='-MCOL2-', relief=sg.RELIEF_GROOVE, vertical_alignment='top', pad=(5, (1, 3)), metadata=DRVStatus)],
[sg.Column(lcol2, vertical_alignment='top',), sg.Column(rcol2, vertical_alignment='top'), sg.Column(rcol2b, vertical_alignment='top')],
[sg.Column(lcol3, vertical_alignment='top'), sg.Column(rcol3, vertical_alignment='top')]]
window = sg.Window('VPCalc', layout, return_keyboard_events=True, finalize=True)
window.TKroot.focus_force() #If the datfile was choosen via the file requester, out window won't have the focus... which is a PITA IMHO, so force it...
#As setting the "default_values" inside above declaration of the listbox element will not scroll to our NIL element, we are updating the listbox here...
window['-LBOX-'].update(scroll_to_index=len(dbase), set_to_index=len(dbase))
return window
# -------------- Queries / Calculations ------------------
def GetParts(numIN):
# * Determining numenator / denumenator from number
# 1st check if it ain't an int
if mp.isint(numIN):
numParts=[str(int(numIN)), "1"]
else:
mpId=mp.identify(numIN)
#**print("Get Parts for: "+mp.nstr(numIN, 21))
#type of mpId may vary
if mpId:
if mpId.count("/")==1:
numParts=mpId.strip("()").split("/")
else:
print("mp.identify couldn't find a simple fraction")
numParts=["-1", "-1"]
else:
print("mp.identify returned 'None'")
#numParts=["-1", "-1"]
print("trying Fraction(s) instead...")
# if App breaks here, or UI starts to look ugly... coding time again :/
Parts=Fraction(float(numIN)).limit_denominator()
numParts=[str(Parts.numerator),str(Parts.denominator)]
#**print("return: ", numParts)
return (numParts)
def QueryRes(xx, yy):
#validate values for target resolution on arbitary limits (besides being even)
#still "valid" results may be problematic for our scaling/fit calculations :/
#ToDo - check at least for standart DARs, don't allow arbitary ones...
# Downscaling may still be problematic though....
if (xx==-1) or (yy==-1):
sg.popup("Can't accept empty fields!")
return False
elif (xx<240) or (yy<240):
sg.popup("Resolution should at least be: 240 x 240!")
return False
elif xx%2!=0 or yy%2!=0:
sg.popup("Only even numbers are acceptable!")
return False
return True
def getEven(this):
#check if the integer part of this is an even number, if not round to nearest even (So far I don't see any benefit of allowing
# uneven pixelsizes for the rounding of fractional pixeldimensions (the error for the DAR is, in both cases, negligible and
# even numbers are far easier to handle (Example: 224*4/4=298.667, nearest INT 299, nearest even INT 298))
#
#get nearest int
that=mp.nint(this)
mZero=mp.mpf("0.0")
if mp.fmod(that, "2.0")!=mZero:
if int(this)%2!=0:
that=mp.nint(this+mp.mpf("0.5"))
else:
that=mp.mpf(int(this))
return that
def QueryDownscale(DAR, sx, sy, framexy, scaleV):
#!!!!We are only checking if the reference axis and the scaled to DAR minimal other axis is fitting our targetframe
#!!!We are not aborting the calculations if that scaled minimal of the other axis is smaller then it is within the source
frameH=mp.mpf(framexy[0])
frameV=mp.mpf(framexy[1])
print("Check Downscale:", sx,"x", sy,"vs.", frameH, "x", frameV)
if scaleV:
print("Query reference: vertical axis ")
if sy>frameV:
print("!!!Target height < source_y")
return True
if frameH<getEven(sy*DAR):
print("!!!Target width < scaled width")
return True
if sx>frameH:
print("***Source_x > scaled width")
else:
print("Query reference: horizontal axis ")
if sx>frameH:
print("!!!Target width < source_x")
return True
if frameV<getEven(sx*(mp.mpf("1.0")/DAR)):
print("!!!Target height < scaled height")
return True
if sy>frameV:
print("***Source_y > scaled height")
return False
def CalcVectorfit(DAR, sx, sy, framexy):
frameH=mp.mpf(framexy[0])
frameV=mp.mpf(framexy[1])
#downscaling ain't a problem here, reference axis may be(?)...
width=getEven(frameV*DAR)
height=frameV
if width>frameH:
#width based on vertical axis exceeds the target frame_width
width=frameV
height=getEven(frameH*(mp.mpf("1.0")/DAR))
if height>frameV:
print("Houston, we have a problem! ... in CalcFit for Vectors")
txt_Frame="Calc x Error"
else:
txt_Frame=str(int(width))+" x "+str(int(height))
scalex=1 #-1
scaley=1 #-1
else:
txt_Frame=str(int(width))+" x "+str(int(height))
scalex=1 #-1
scaley=1 #-1
return (txt_Frame, scalex, scaley)
def CalcFit(DAR, sx, sy, framexy, scaleV):
frameH=mp.mpf(framexy[0])
frameV=mp.mpf(framexy[1])
if scaleV:
#Vertical Axis is ref
print("n-scaling: vertical axis ")
max_n=mp.floor(frameV/sy)
height=sy*max_n
width=getEven(height*DAR)
while width>frameH:
max_n-=1 #Shouldn't become 0 (QueryDownscale verified n=1 case)
height=sy*max_n
width=getEven(height*DAR)
scaley=max_n
scalex=width/sx
else:
#Horizontal Axis is ref
print("n-scaling: horizontal axis ")
max_n=mp.floor(frameH/sx)
width=sx*max_n
height=getEven(width*(mp.mpf("1.0")/DAR))
while height>frameV:
max_n-=1
width=sx*max_n
height=getEven(width*(mp.mpf("1.0")/DAR))
scalex=max_n
scaley=height/sy
txt_Frame=str(int(width))+" x "+str(int(height))
return (txt_Frame, scalex, scaley)
def CalcMax(DAR, sx, sy, framexy):
#Ok, I bet there is a slick and neat way to deal with that (for all possible target res),
#for now we are doing it step by step and add/change if needed....
tx=mp.mpf(framexy[0])
ty=mp.mpf(framexy[1])
height=ty
width=getEven(ty*DAR )
if width>tx:
#calculated width exceeds display resolution
width=tx
height=getEven(tx*(mp.mpf("1.0")/DAR))
if height>ty:
#is this possible???? [late night/early morning question]
print("SCRIPT-ERROR: uncaptured CalcMax case")
return ("Calc x Max Error", "-1.0","-1.0")
txt_Frame=str(int(width))+" x "+str(int(height))
if sx==-1: #check for vector screen (could have well been sy to query upon)
scalex=1 #-1
scaley=1 #-1
else:
scalex=width/sx
scaley=height/sy
return (txt_Frame, scalex, scaley)
def CalcFrame(DAR, axis, nscale):
#nScale the reference axis and calculate the othe one from DAR
RefAx=axis*nscale
OtherAx=getEven(RefAx*DAR)
return (str(int(RefAx)), str(int(OtherAx)))
# ----------------- UI Updates ------------------
# Display for invalid rom names
def BlankInfo(window):
#No valid 'rom' set, so we need to blank our displayed information
window['-romdesc-'].update(value="NIL - the missing rom")
window['-CAT-'].update(value="NIL")
#window['-cloneof-'].update(value="Clone of: lostrom")
window['-CLONE-'].update(value="-")
window['-CHD-'].update(value="-----")
window['-SAMPLE-'].update(value="-")
window['-screen-'].update(value="Screen: none [none]")
window['-DAR_text-'].update(value="DAR = # / # = #.#")
window['-SAR_xy-'].update(value="Source: ... x ... px")
window['-SAR_ratio-'].update(value="SAR = # / #")
window['-SAR_val-'].update(value=" = #.#")
window['-PAR_ratio-'].update(value="PAR = # / #")
window['-PAR_val-'].update(value=" = #.#")
window['-SimpleSize-'].update(value="SRC to 1/1 PAR: ... x ...")
window['-CALC_XY-'].update(value="nScaled_Frame: ... x ...")
window['-SCALE_X-'].update(value="Scale: (Source X) x #.#")
window['-SCALE_Y-'].update(value="Scale: (Source Y) x #.#")
window['-DCALC_XY-'].update(value="max_Frame: ... x ...")
window['-DSCALE_X-'].update(value="Scale: (Source X) x #.#")
window['-DSCALE_Y-'].update(value="Scale: (Source Y) x #.#")
if window['-MCOL2-'].metadata==1:
window['-DRIVER-'].update(value="-")
window['-GFX-'].update(value="-")
window['-SND-'].update(value="-")
else:
window['-DRIVER-'].update(value="- (-)")
if window['-MCOL2-'].metadata==2:
window['-GFX-'].update(value="-")
window['-SND-'].update(value="-")
window['-nScale-'].update(value=0, values=[0])
def UpdateCalc(window, dbase, rIdx, ndigits, ltxt, VPxy, scaleV):
#Calculate our (n)scaled Viewport
print("Update calculations for:", dbase["ROM"][rIdx])
sx=mp.mpf(dbase["sx"][rIdx])
sy=mp.mpf(dbase["sy"][rIdx])
if dbase["H|V"][rIdx]=="H":
DAR=mp.fraction(4,3)
else:
DAR=mp.fraction(3,4)
if dbase["Screen"][rIdx]=="vector":
txt_Calc=CalcVectorfit(DAR, sx, sy, VPxy)
elif QueryDownscale(DAR, sx, sy, VPxy, scaleV):
#integer downscaling of the reference axis ain't possible
txt_Calc=["Calc x Error (Downscaling)", "#.#","#.#"]
else: #Screentype is Raster and simple n=1 scaling of the reference axis is valid
txt_Calc=CalcFit(DAR, sx, sy, VPxy, scaleV)
txt_DCalc=CalcMax(DAR, sx, sy, VPxy)
window['-CALC_XY-'].update(value="nScaled_Frame: "+txt_Calc[0])
window['-SCALE_X-'].update(value="Scale: (Source X) x "+mp.nstr(txt_Calc[1], ndigits))
window['-SCALE_Y-'].update(value="Scale: (Source Y) x "+mp.nstr(txt_Calc[2], ndigits))
window['-DCALC_XY-'].update(value="max_Frame: "+txt_DCalc[0])
window['-DSCALE_X-'].update(value="Scale: (Source X) x "+mp.nstr(txt_DCalc[1], ndigits))
window['-DSCALE_Y-'].update(value="Scale: (Source Y) x "+mp.nstr(txt_DCalc[2], ndigits))
return txt_Calc
def setInfo_nDis(window, dbase, rIdx):
BlankInfo(window)
#20201126 Update the UI for multidisplay Item
# Lines below are modified copyNpastes form UpdateInfo
#20201205 Excluded Screen!=Raster|Vector from the calculation
if dbase["multidisplay"][rIdx]=="False":
window['-screen-'].update(value="Screen: "+dbase["Screen"][rIdx])
txt_SARxy="Source: "+dbase["sx"][rIdx]+" x "+dbase["sy"][rIdx]+" (?Pixel?)"
window['-SAR_xy-'].update(value=txt_SARxy)
else:
window['-screen-'].update(value="Screen: multiple Displays")
cloneof=dbase["cloneof"][rIdx]
if cloneof=="none":
cloneof="-"
sampleset=dbase["sampleof"][rIdx]
if sampleset=="none":
sampleset="-"
CHD=dbase["chd"][rIdx]
if CHD=="none":
CHD="-"
#CHD textelement is of size 36 (see SetupUI)
CHDtooltip=CHD
if len(CHD)>36:
CHD=CHD[0:32]+"..."
driver=dbase["driver"][rIdx]
if window['-MCOL2-'].metadata==0:
driver+=" ("+dbase["sound"][rIdx]+")"
elif window['-MCOL2-'].metadata in (2, 3):
driver+=" ("+dbase["emulation"][rIdx]+")"
category=dbase["Category"][rIdx]
#... Do we got subtypes?
if (dbase["Subtype"][rIdx]!="<_generic_>"):
category+=" - "+str(dbase["Subtype"][rIdx])
#dtype was set to str in reading the database, so no boolean compare, but to string instead
if dbase["Mature"][rIdx]=="True":
category+=" *MATURE*"
window['-romdesc-'].update(value="\n".join(textwrap.wrap(dbase["Desc"][rIdx], width=64)))
window['-CLONE-'].update(value=cloneof)
window['-CAT-'].update(value=category)
window['-SAMPLE-'].update(value=sampleset)
window['-CHD-'].update(value=CHD)
window['-CHD-'].SetTooltip(CHDtooltip)
window['-DRIVER-'].update(value=driver)
if window['-MCOL2-'].metadata in (1, 2):
window['-GFX-'].update(dbase["graphic"][rIdx])
window['-SND-'].update(dbase["sound"][rIdx])
# Display the Information for an actual rom
def UpdateInfo(window, dbase, rIdx, ndigits, ltxt, VPxy, DRVStatus): # UpdateInfo returns scaleV
#variables: window element, rom database, roms Listindex, significant digits to print, maximal length for the output string
#**print("UpdateInfo() invoked. Digits to print: "+str(ndigits))
#sane ltxt value?
if ltxt<14: #assume space for at least ndigits of 6
ltxt=14
elif ltxt>103: # totally arbitrary
ltxt=103
#limit for ndigits: ltxt - 7 >> rowtitle (6 chars) + radix point (1 char)
# and (assuming that the integer part of the fraction ain't >1 digit, ndigits should at least be 2
if ndigits < 2:
ndigits=2
elif ndigits > (ltxt-7):
ndigits=ltxt-7
#Display the current roms full description (human readable title)
window['-romdesc-'].update(value="\n".join(textwrap.wrap(dbase["Desc"][rIdx], width=64)))
#Build informal strings and update the UI
txt_screen="Screen: "
txt_DAR="DAR = "
txt_SARratio="SAR = "
txt_PARratio="PAR = "
txt_SARval=" = "
txt_PARval=" = "
txt_SimpleSize="SRC to 1/1 PAR: "
sx=dbase["sx"][rIdx]
sy=dbase["sy"][rIdx]
simple_x=mp.mpf(sx)
simple_y=mp.mpf(sy)
cloneof=dbase["cloneof"][rIdx]
if cloneof=="none":
cloneof="-"
sampleset=dbase["sampleof"][rIdx]
if sampleset=="none":
sampleset="-"
CHD=dbase["chd"][rIdx]
if CHD=="none":
CHD="-"
#CHD textelement is of size 36 (see SetupUI)
CHDtooltip=CHD
if len(CHD)>36:
CHD=CHD[0:32]+"..."
driver=dbase["driver"][rIdx]
if window['-MCOL2-'].metadata==0:
driver+=" ("+dbase["sound"][rIdx]+")"
elif window['-MCOL2-'].metadata in (2, 3):
driver+=" ("+dbase["emulation"][rIdx]+")"
category=dbase["Category"][rIdx]
#... Do we got subtypes?
if (dbase["Subtype"][rIdx]!="<_generic_>"):
category+=" - "+str(dbase["Subtype"][rIdx])
#dtype was set to str in reading the database, so no boolean compare, but to string instead
if dbase["Mature"][rIdx]=="True":
category+=" *MATURE*"
#Simple Setup of the Basic Information
txt_SARxy="Source: "+sx+" x "+sy+" (Pixel)"
#For the displayed DAR Information we don't need any variable, as we are either using 4:3 or 3:4, depending on the orientation of the game [H/V]
#though it is still needed later to calculate the PAR
if dbase["H|V"][rIdx]=="H":
txt_angle=" [Horizontal]"
txt_DAR+=" 4 / 3 = 1.333333333..."
DAR=mp.fraction(4, 3)
window["-AXIS-"].update(value=" vertical")
scaleV=True
else:
txt_angle=" [Vertical]"
txt_DAR+=" 3 / 4 = 0.75"
DAR=mp.fraction(3, 4)
window["-AXIS-"].update(value=" horizontal")
scaleV=False
if dbase["Screen"][rIdx]=="vector":
txt_screen+="Vector"+txt_angle
txt_SARxy=""
#thats it, no further info for vector based games needed
else: #Screentype is Raster (?) and simple n=1 scaling of the reference axis is valid
txt_screen+="Raster"+txt_angle
SAR=simple_x/simple_y
SARparts=GetParts(SAR)
#https://en.wikipedia.org/wiki/Pixel_aspect_ratio
PAR=DAR/SAR
PARparts=GetParts(PAR)
#(for now no len check/restriction for the fractionals)
#But make sure we got valid ones
if "-1" in SARparts:
txt_SARratio+="couldn't compute a fraction for it"
else:
txt_SARratio+=" / ".join(SARparts)
if "-1" in PARparts:
txt_PARratio+="couldn't compute a fraction for it"
else:
txt_PARratio+=" / ".join(PARparts)
txt_SARval+=mp.nstr(SAR, ndigits)
txt_PARval+=mp.nstr(PAR, ndigits)
#Simple Resize of the SAR to 1:1 PAR
if txt_angle==" [Horizontal]":
simple_x=simple_y*DAR
#**("Simple: ",mp.nstr(simple_x,6)," x ",mp.nstr(simple_y,6)," AR=", (simple_x/simple_y))
simple_x=getEven(simple_x)
print("Simple: ",mp.nstr(simple_x,6)," x ",mp.nstr(simple_y,6)," AR=", mp.nstr((simple_x/simple_y), 8))
else:
simple_y=simple_x*(mp.mpf("1.0")/DAR)
#**print("Simple: ",simple_x," x ",mp.nstr(simple_y,6)," AR=", (simple_x/simple_y))
simple_y=getEven(simple_y)
print("Simple: ",simple_x," x ",mp.nstr(simple_y,6)," AR=", mp.nstr((simple_x/simple_y), 8))
txt_SimpleSize+=(str(int(simple_x))+" x "+str(int(simple_y)))
#finally update the UI
window['-screen-'].update(value=txt_screen)
window['-DAR_text-'].update(value=txt_DAR)
window['-SAR_xy-'].update(value=txt_SARxy)
window['-SAR_ratio-'].update(value=txt_SARratio)
window['-SAR_val-'].update(value=txt_SARval)
window['-PAR_ratio-'].update(value=txt_PARratio)
window['-PAR_val-'].update(value=txt_PARval)
window['-SimpleSize-'].update(value=txt_SimpleSize)
window['-CLONE-'].update(value=cloneof)
window['-CAT-'].update(value=category)
window['-SAMPLE-'].update(value=sampleset)
window['-CHD-'].update(value=CHD)
window['-CHD-'].SetTooltip(CHDtooltip)
window['-DRIVER-'].update(value=driver)
if window['-MCOL2-'].metadata in (1, 2):
window['-GFX-'].update(dbase["graphic"][rIdx])
window['-SND-'].update(dbase["sound"][rIdx])
return scaleV
#Set new values for the targeted resolution
def SetTarget(tx, ty):
layout=[[sg.Text(text="Set Target Frame")],
[sg.Text(text="width = "), sg.InputText(key="-IN_TX-", enable_events=True, size=(8, 1))],
[sg.Text(text="height = "), sg.InputText(key="-IN_TY-", enable_events=True, size=(8, 1))],
[sg.Button(button_text="SET", key="-TSet-"), sg.Button(button_text="Cancel", key="-TCancel-")]]
window=sg.Window("VPC: Set Target Size", layout, disable_minimize=True, modal=True, finalize=True)
while True:
event, values = window.read()
#**print("SetTargetEvents>> ", event, values)
if (event == sg.WIN_CLOSED) or (event== "-TCancel-"):
break
#Allow only numbers as input, restrict numer of digits to 5
if event == '-IN_TX-':
if values['-IN_TX-'] and values['-IN_TX-'][-1] not in ('0123456789'):
window['-IN_TX-'].update(values['-IN_TX-'][:-1])
elif len(values['-IN_TX-'])>5:
window['-IN_TX-'].update(values['-IN_TX-'][:-1])
elif event == '-IN_TY-':
if values['-IN_TY-'] and values['-IN_TY-'][-1] not in ('0123456789'):
window['-IN_TY-'].update(values['-IN_TY-'][:-1])
elif len(values['-IN_TY-'])>5:
window['-IN_TY-'].update(values['-IN_TY-'][:-1])
elif event=="-TSet-":
txt=window['-IN_TX-'].Get()
if len(txt)>0:
rx=int(txt)
else:
rx=-1
txt=window['-IN_TY-'].Get()
if len(txt)>0:
ry=int(txt)
else:
ry=-1
#Return new values only if acceptable, otherwise stay open
if QueryRes(rx, ry):
tx=rx
ty=ry
break
window.make_modal()
window.close()
return (str(tx), str(ty))
def UpdateScale(window, dbase, nscale, scaleV, Idx, ndigits):
sx=mp.mpf(dbase["sx"][Idx])
sy=mp.mpf(dbase["sy"][Idx])
DAR=mp.fraction("4","3") #For original reference axis
if scaleV:
#inverted DAR for changed reference axis
if dbase["H|V"][Idx]=="V":
DAR=mp.fraction("3","4")
VP=CalcFrame(DAR, sy, nscale)
txtFrame=VP[1]+" x "+VP[0]
scaleY=mp.mpf(str(nscale))
scaleX=mp.mpf(VP[1])/sx
else:
#inverted DAR for changed reference axis
if dbase["H|V"][Idx]=="H":
DAR=mp.fraction("3","4")
VP=CalcFrame(DAR, sx, nscale)
txtFrame=VP[0]+" x "+VP[1]
scaleX=mp.mpf(str(nscale))
scaleY=mp.mpf(VP[1])/sy
window['-CALC_XY-'].update(value="nScaled_Frame: "+txtFrame)
window['-SCALE_X-'].update(value="Scale: (Source X) x "+mp.nstr(scaleX, ndigits))
window['-SCALE_Y-'].update(value="Scale: (Source Y) x "+mp.nstr(scaleY, ndigits))
return txtFrame
# ------------------
# -- MAIN APP --
# ------------------
def VPCalc(database, targetxy=["1600", "1200"]):
#Setting up some base variables
dbItems = len(database["ROM"])
LBoxIdx = dbItems #selected item in listbox
srcVP="1600 x 1200" #Viewport Dimensions (for Copy 2 Clipboard Info)
#Setup the UI (ltxtrcol shouldn't be lower then 42)
ltxtrcol = 44 #Size for the textfields within the right column
window = SetupUI(database,targetxy[0],targetxy[1])
BlankInfo(window)
DRVStatus=window['-MCOL2-'].metadata
kip_prev=window['-ROM_IN-'].Get() #set initial kip_prev outside of the loop
sigDigits=34 #number of digits to print
scaleV=True
# ------------- Start Event Loop -------------
while True:
event, values = window.read()
print("Event read:", event, values)
# reset some variables
updateINFO=True #Update Rom Information?
updateCALC=False #Update our Calculations?
updateSCALE=False #Update nScale Calculation?
kip=window['-ROM_IN-'].Get()
lkip=len(kip)
#print("prev_kip:"+str(kip_prev)+", kip: "+str(kip))
#Check kip for valid characters (*), undo unwanted changes
# * >> so far: alphanumerical + underscore
if not bool(re.match("^[A-Za-z0-9_]*$", kip)):
print("Removed Invalid Character from kip")
kip=kip_prev
window['-ROM_IN-'].update(kip)
#window closed >> terminate app? [20201206 needed to split this up, because the following check would fail if the winow was closed via the UI]
if (event == sg.WIN_CLOSED):
break
#After finding out, that running the script under ubuntu (20.04) does not only mess up with the layout,
#but also that the keycodes (numerical part) are not identical, we are going to ommit them:
if ":" in event:
orgEvent=event
event="!"+event.split(":")[0]
print("Modified",orgEvent," event to:", event)
#ESC pressed? >> terminate app?
if event=="!Escape":
break
# CURSOR UP pressed
if event=='!Up':
#lets move one item up within the listbox
if LBoxIdx>1:
LBoxIdx-=1
#update elements
window['-ROM_IN-'].update(window['-LBOX-'].GetListValues()[LBoxIdx])
window['-LBOX-'].update(scroll_to_index=LBoxIdx, set_to_index=LBoxIdx)
# CURSOR DOWN pressed
elif event=="!Down":
#lets move one item down within the listbox
#should we allow or disallow moving to the NIL item???? -> for now, allow it
if LBoxIdx<dbItems:
LBoxIdx+=1
#update elements
if LBoxIdx==dbItems:
window['-ROM_IN-'].update("")
else:
window['-ROM_IN-'].update(window['-LBOX-'].GetListValues()[LBoxIdx])
window['-LBOX-'].update(scroll_to_index=LBoxIdx, set_to_index=LBoxIdx)
elif event=="!Home":
LBoxIdx=1
window['-ROM_IN-'].update(window['-LBOX-'].GetListValues()[LBoxIdx])
window['-LBOX-'].update(scroll_to_index=LBoxIdx, set_to_index=LBoxIdx)
elif event=="!End":
LBoxIdx=dbItems-1
window['-ROM_IN-'].update(window['-LBOX-'].GetListValues()[LBoxIdx])
window['-LBOX-'].update(scroll_to_index=LBoxIdx, set_to_index=LBoxIdx)
elif event=="!Prior":
LBoxIdx-=7
if LBoxIdx<1:
LBoxIdx=1
window['-ROM_IN-'].update(window['-LBOX-'].GetListValues()[LBoxIdx])
window['-LBOX-'].update(scroll_to_index=LBoxIdx, set_to_index=LBoxIdx)
elif event=="!Next":
LBoxIdx+=7
if LBoxIdx>dbItems:
LBoxIdx=dbItems
window['-ROM_IN-'].update(window['-LBOX-'].GetListValues()[LBoxIdx])
window['-LBOX-'].update(scroll_to_index=LBoxIdx, set_to_index=LBoxIdx)
# UI EVENT Listbox
elif event == '-LBOX-':
LBoxIdx=window[event].GetIndexes()[0]
#**print("-LBOX- >>",LBoxIdx, values[event][0])
#Update Inputfield and return focus to it
if LBoxIdx<dbItems:
kip=values[event][0]
else:
kip=""
window['-ROM_IN-'].update(kip)
window['-ROM_IN-'].SetFocus()
# UI Event Copy Config/Viewport Info to Clipboard
elif event == '-CVP-':
print(srcVP)
VPaxis=srcVP.strip().split(" x ")
# Lets build the VP String
VPtxt="#VPC-Viewport for rom: "+database["ROM"][LBoxIdx]+"\n"
VPtxt+="# 20 = Config, 21 = 1:1 PAR, 22 = Core Provided, 23 = Custom Viewport\naspect_ratio_index = \"23\"\n\n"
VPtxt+="# these two define the pixel size of the emulated screen\n"
VPtxt+="custom_viewport_width = \""+VPaxis[0]+"\"\n"
VPtxt+="custom_viewport_height = \""+VPaxis[1]+"\"\n\n"
VPtxt+="# the following two decide how far from the left and top the game screen is shown\n"
#so far I am not seeing any case where the division by 2 may result in a non int (as the target Dimensons and calculated VPxy results are bound to be even)
VPtxt+="# ("+targetxy[0]+" - "+VPaxis[0]+") / 2 =\n"
VPtxt+="custom_viewport_x = \""+str(int((int(targetxy[0])-int(VPaxis[0]))/2))+"\"\n"
VPtxt+="# ("+targetxy[1]+" - "+VPaxis[1]+") / 2 =\n"
VPtxt+="custom_viewport_y = \""+str(int((int(targetxy[1])-int(VPaxis[1]))/2))+"\"\n"
# and copy it to the clipboard
clip.copy(VPtxt)
updateINFO=False
# UI EVENT set Axis for scaling
elif event == '-AXIS-':
print("changed axis for scaling?")
if (values['-AXIS-']==" vertical" and scaleV==False) or (values["-AXIS-"]==' horizontal' and scaleV==True):
print("axis changed!")
scaleV=not scaleV
if LBoxIdx<dbItems:
updateCALC=True
else:
print("not changed!")
updateINFO=False
window['-ROM_IN-'].SetFocus()
# UI EVENT SetTarget
elif event=="-TARGET-":
targetxy=SetTarget(targetxy[0], targetxy[1])
window['-TARGETED-'].update("Targeted Display/Frame: "+targetxy[0]+" x "+targetxy[1])
#ToDo -> seperate updates, here we just need the CalcFit Values...
elif event=="-nScale-":
if database["Screen"][LBoxIdx]!="vector":
nscaleis=int(values['-nScale-'])
window['-nScale-'].update(value=nscaleis)
updateINFO=False
updateSCALE=True
else:
updateINFO=False
window['-ROM_IN-'].SetFocus()
# UI EVENT InputText (simulated)
elif kip_prev!=kip:
#romname changed in input field ??
#Find 1st matching romname for the text typed so far
LBoxIdx =next((i for i, test in enumerate(database["ROM"]) if kip in test[0:lkip]), -1)
if LBoxIdx ==-1 or kip=='':
#print(dbItems,": Not in List!")
LBoxIdx=dbItems
else:
print("-ROM_IN- >>",LBoxIdx,": "+database["ROM"][LBoxIdx])
window['-LBOX-'].update(scroll_to_index=LBoxIdx, set_to_index=LBoxIdx)
else:
updateINFO=False
kip_prev=kip #save current kip value for the next Event
# Updating rom information if needed
if updateINFO:
#do we need dummy values, because our target is NIL?
if LBoxIdx==dbItems:
BlankInfo(window)
window['-CVP-'].update(disabled=True)
#no, so lets update the fields with some real information!
else:
if database["Screen"][LBoxIdx]!="raster":
window['-nScale-'].update(disabled=True)
window['-AXIS-'].update(disabled=True)
else:
window['-nScale-'].update(disabled=False)
window['-AXIS-'].update(disabled=False)
if database["multidisplay"][LBoxIdx]=="False":
#enable copyVPC
window['-CVP-'].update(disabled=False)
scaleV=UpdateInfo(window, database, LBoxIdx, sigDigits, ltxtrcol, targetxy, DRVStatus)
updateCALC=True
else:
#disable copyVPC
window['-CVP-'].update(disabled=True)
setInfo_nDis(window, database, LBoxIdx)
if updateCALC:
calcScale=UpdateCalc(window, database, LBoxIdx, 8, ltxtrcol, targetxy, scaleV)
srcVP=calcScale[0]
if database["Screen"][LBoxIdx]=="vector":
nscaleis=0
window['-nScale-'].update(value=nscaleis, values=[0])
elif database["Screen"][LBoxIdx]=="raster":
if scaleV:
nscaleis=int(calcScale[2])
window['-nScale-'].update(value=nscaleis, values=list(range(nscaleis, 0, -1)))
else:
nscaleis=int(calcScale[1])
window['-nScale-'].update(value=nscaleis, values=list(range(nscaleis, 0, -1)))
else:
print("Screen!=Raster|Vector")
window['-CVP-'].update(disabled=True)
setInfo_nDis(window, database, LBoxIdx)
if updateSCALE:
if database["multidisplay"][LBoxIdx]=="False":
srcVP=UpdateScale(window, database, nscaleis, scaleV, LBoxIdx, 8)
else:
print("updateSCALE called for multidisplay item!") #Debug Info....
#send an empty line to the terminal
print()
#Terminate Application
window.close()
def VPC_StartUp(infile):
#ToDo: (Optional) exclusion of all roms from the database where VPC won't calculate a viewport (wrong screentype, multidisplay, etc.)
print("Reading information from "+infile)
DBase = pd.read_csv(infile,sep="\t", dtype=str, na_filter=False) #dtype=str means columns!=BOOL
DBase.sort_values(by=["ROM"], inplace=True, ignore_index=True)
#ToDo: Check if all needed "columns" (headers) are present
#....
AllValid=True
n=2 #line 1: headers
for item in DBase["ROM"]:
if not bool(re.match("^[A-Za-z0-9_]*$", item)):
print("Rom Entry (line",n, "):", item, " contains unsupported characters!")
AllValid=False
n+=1
if AllValid==True:
VPCalc(DBase)
else:
print("Aborted, due to encountered errors within:", infile)
# -------------
# -- MAIN --
# -------------
#ToDo Filecheck...
#read the database into a pandas dataframe, using 1st row as the column headers (default), and as we are using mpmath: read all columns/values as strings
#ToDo check database rom names for "invalid" characters - and notify the user about it (either extend the check for typed characters inside the VPCalc
#loop, or remove the item from the database (within the sourcefile or from our read in values) .....
#
#Execute only if not imported, after all & how unlikely it is, I or someone else may import this to run some kind of batch processing over a datfile...
if __name__=="__main__":
infile = sys.argv[1] if len(sys.argv) > 1 else sg.popup_get_file('Choose VPC_ReferenceDat')
#Test if file exists
if os.path.isfile(infile):
VPC_StartUp(infile)
else:
print("Cannot find "+infile+"!")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment