241 lines
9.6 KiB
Python
241 lines
9.6 KiB
Python
from rules import *
|
|
|
|
def patch_shader(fname, f_lines, game_settings_module=None, enable_print_warnings=True):
|
|
f_lines[0] += "#" + fname +"\n"
|
|
|
|
|
|
# do attrib, output, param simplification and replace header tags
|
|
attribs = {}
|
|
outputs = {}
|
|
params = {}
|
|
|
|
for i in range(0, len(f_lines)):
|
|
# this inlining stuff is only really needed for vp, but :shrug:
|
|
no_repl = False
|
|
|
|
m = ATTRIB_REGEX.match(f_lines[i])
|
|
while m:
|
|
attribs[m.group(3)] = m.group(4)
|
|
f_lines[i] = ATTRIB_REGEX.sub('#\\1 \n', f_lines[i], count=1)
|
|
no_repl = True
|
|
m = ATTRIB_REGEX.match(f_lines[i])
|
|
|
|
m = OUTPUT_REGEX.match(f_lines[i])
|
|
while m:
|
|
outputs[m.group(3)] = m.group(4)
|
|
f_lines[i] = OUTPUT_REGEX.sub('#\\1 \n', f_lines[i], count=1)
|
|
no_repl = True
|
|
m = OUTPUT_REGEX.match(f_lines[i])
|
|
|
|
# and not needed at all for PARAMs in the end, but :shrug: again
|
|
m = PARAM_REGEX.match(f_lines[i])
|
|
while m:
|
|
params[m.group(3)] = m.group(4)
|
|
f_lines[i] = PARAM_REGEX.sub('#\\1 \n', f_lines[i], count=1)
|
|
no_repl = True
|
|
m = PARAM_REGEX.match(f_lines[i])
|
|
|
|
if no_repl:
|
|
continue
|
|
|
|
replace_tokens = attribs.copy()
|
|
replace_tokens.update(outputs)
|
|
replace_tokens.update(params)
|
|
|
|
# sort from longest to shortest to minimize risk of names that are substrings of others causing issues
|
|
for k in sorted(replace_tokens.keys(), reverse=True, key=lambda s: len(s)):
|
|
# look for specific preceding characters to not replace in the middle of a name
|
|
f_lines[i] = f_lines[i].replace(' ' + k, ' ' + replace_tokens[k])
|
|
f_lines[i] = f_lines[i].replace('-' + k, '-' + replace_tokens[k])
|
|
f_lines[i] = f_lines[i].replace('{' + k, '{' + replace_tokens[k])
|
|
|
|
# options are required for conditional stuff...
|
|
# tysm nezarn for teaching me this because otherwise the patch would never be anywhere near as complete
|
|
f_lines[i] = NVFP_HEADER_REGEX.sub("!!ARBfp1.0\nOPTION NV_fragment_program2;", f_lines[i])
|
|
f_lines[i] = NVVP_HEADER_REGEX.sub("!!ARBvp1.0\nOPTION NV_vertex_program3;", f_lines[i])
|
|
|
|
f_full = ''.join(f_lines)
|
|
|
|
f_full = UNUSED_PARAM_REGEX.sub("#\\1 \n", f_full)
|
|
|
|
|
|
# add a temp vars for NRM and SSG replacements before their first use
|
|
f_full = f_full.replace('NRM ', 'TEMP nrm_tmp; NRM ', 1)
|
|
f_full = f_full.replace('SSG ', 'TEMP ssg_tmp1, ssg_tmp2; SSG ', 1)
|
|
# this needs to use the regex to match because it isn't just a single instruction
|
|
f_full = SAMPLER_OFFSET_REGEX.sub('TEMP tex_offset_coord; \\g<0>', f_full, count=1)
|
|
|
|
for r in fix_repls:
|
|
if r[0].match(fname):
|
|
if r[3]: # r[3]: apply until no matches
|
|
while r[1].search(f_full):
|
|
f_full = r[1].sub(r[2], f_full)
|
|
else:
|
|
f_full = r[1].sub(r[2], f_full)
|
|
|
|
if game_settings_module and game_settings_module.fix_repls:
|
|
for r in game_settings_module.fix_repls:
|
|
if r[0].match(fname):
|
|
if r[3]: # r[3]: apply until no matches
|
|
while r[1].search(f_full):
|
|
f_full = r[1].sub(r[2], f_full)
|
|
else:
|
|
f_full = r[1].sub(r[2], f_full)
|
|
|
|
|
|
f_full = f_full.replace('INT TEMP ', 'TEMP ')
|
|
|
|
if game_settings_module and game_settings_module.post_filter:
|
|
f_full = game_settings_module.post_filter(fname, f_full)
|
|
|
|
|
|
bad_instr = BAD_INSTR_REGEX.search(f_full)
|
|
if bad_instr:
|
|
if enable_print_warnings:
|
|
print ('\rWarning -- unpatched known bad instruction(s) in {} -- first: {}'.format(fname, bad_instr.group(1)))
|
|
f_full = BAD_INSTR_REGEX.sub("#\\1 \n", f_full)
|
|
|
|
f_full = f_full.replace('\r', '')
|
|
|
|
return f_full
|
|
|
|
if __name__ == '__main__':
|
|
from os import listdir, makedirs, get_terminal_size
|
|
from os.path import join as joinpath, splitext, isfile, exists, dirname
|
|
from re import compile as recompile
|
|
import sys
|
|
import importlib, importlib.util
|
|
|
|
if getattr(sys, 'frozen', False):
|
|
datadir = dirname(sys.executable)
|
|
else:
|
|
datadir = dirname(__file__)
|
|
|
|
gamesettings_dir = joinpath(datadir, 'gamesettings')
|
|
|
|
def get_args():
|
|
# make a list of game settings modules
|
|
game_modules_list = [f[:-3] for f in listdir(gamesettings_dir) if isfile(joinpath(gamesettings_dir, f)) and splitext(f)[1] == '.py']
|
|
|
|
import argparse
|
|
parser = argparse.ArgumentParser(description='AMD ARB Patcher: An attempt at converting Nvidia-only ARB shaders to work on AMD. Patching techniques from Nezarn; implementation by somewhatlurker.')
|
|
parser.add_argument('-i', '--in_dir', default='shader in', help='input directory containing .vp and .fp files (default: "shader in")')
|
|
parser.add_argument('-o', '--out_dir', default='shader patched', help='output directory (default: "shader patched")')
|
|
parser.add_argument('-f', '--file_filter', default=None, help='only process files that match this filter (regex)')
|
|
parser.add_argument('-g', '--game_settings', default=None, choices=game_modules_list, help='game-specific settings to load')
|
|
|
|
parser.add_argument('--list_games', action='store_true', help='list game names for game-specific settings options instead of patching shaders')
|
|
|
|
return parser.parse_args()
|
|
|
|
def print_game_modules():
|
|
# make a list of game settings modules
|
|
game_modules_list = [f[:-3] for f in listdir(gamesettings_dir) if isfile(joinpath(gamesettings_dir, f)) and splitext(f)[1] == '.py']
|
|
|
|
for game in game_modules_list:
|
|
game_settings_spec = importlib.util.spec_from_file_location(game, joinpath(gamesettings_dir, game + '.py'))
|
|
game_settings_module = importlib.util.module_from_spec(game_settings_spec)
|
|
game_settings_spec.loader.exec_module(game_settings_module)
|
|
|
|
if game_settings_module.GAME_NAME:
|
|
full_name = game_settings_module.GAME_NAME
|
|
else:
|
|
full_name = 'Unknown'
|
|
|
|
print ('{}: {}'.format(game, full_name))
|
|
|
|
|
|
args = get_args()
|
|
|
|
if args.list_games:
|
|
print_game_modules()
|
|
sys.exit()
|
|
|
|
print("AMD ARB Patcher")
|
|
print("===============")
|
|
print("Patching techniques from Nezarn; implementation by somewhatlurker")
|
|
print("=================================================================")
|
|
print("Input directory: '{}'".format(args.in_dir))
|
|
print("Output directory: '{}'".format(args.out_dir))
|
|
|
|
if args.game_settings:
|
|
game_settings_spec = importlib.util.spec_from_file_location(args.game_settings, joinpath(gamesettings_dir, args.game_settings + '.py'))
|
|
game_settings_module = importlib.util.module_from_spec(game_settings_spec)
|
|
game_settings_spec.loader.exec_module(game_settings_module)
|
|
if game_settings_module.GAME_NAME:
|
|
print("Using game-specific settings for {}".format(game_settings_module.GAME_NAME))
|
|
else:
|
|
print("Using game-specific settings for {}".format(args.game_settings))
|
|
else:
|
|
game_settings_module = None
|
|
|
|
IN_DIR = args.in_dir
|
|
OUT_DIR = args.out_dir
|
|
|
|
if not exists(IN_DIR):
|
|
print ("Directory '{}' does not exist. Aborting".format(IN_DIR))
|
|
sys.exit()
|
|
elif isfile(IN_DIR):
|
|
print ("'{}' is not a directory. Aborting".format(IN_DIR))
|
|
sys.exit()
|
|
|
|
if exists(OUT_DIR):
|
|
if isfile(OUT_DIR):
|
|
print ("'{}' already exists but is not a directory. Aborting".format(OUT_DIR))
|
|
sys.exit()
|
|
else:
|
|
makedirs(OUT_DIR)
|
|
|
|
if args.file_filter:
|
|
file_filter = recompile(args.file_filter)
|
|
else:
|
|
file_filter = None
|
|
|
|
indir_list = listdir(IN_DIR)
|
|
|
|
if file_filter:
|
|
indir_list = [f for f in indir_list if file_filter.match(f)]
|
|
|
|
proc_count = 0
|
|
|
|
last_status_len = 0
|
|
|
|
for fname in indir_list:
|
|
proc_count += 1
|
|
progress_val = proc_count / len(indir_list)
|
|
progress_cnt_X = int(progress_val * 20)
|
|
|
|
status_str = '\r[{e:{s1}<{n1}}{e:{s2}<{n2}}]'.format(e='', s1='X', s2='-', n1=progress_cnt_X, n2=20-progress_cnt_X)
|
|
status_str += ' {:.2%}'.format(progress_val)
|
|
status_str += ' ' + fname
|
|
|
|
try:
|
|
terminal_width = get_terminal_size()[0]
|
|
except:
|
|
terminal_width = 120
|
|
|
|
if len(status_str) > terminal_width:
|
|
status_str = status_str[:terminal_width-3] + '...'
|
|
|
|
# fix characters left on screen from a previous longer line
|
|
# (without cursor staying off to the side)
|
|
this_status_len = len(status_str)
|
|
if this_status_len < last_status_len:
|
|
status_str = '{: <{l}}'.format(status_str, l=last_status_len)
|
|
last_status_len = this_status_len
|
|
|
|
print (status_str, end='')
|
|
|
|
if game_settings_module and game_settings_module.filename_filter:
|
|
openname = game_settings_module.filename_filter(fname)
|
|
else:
|
|
openname = fname
|
|
|
|
with open(joinpath(IN_DIR, openname), 'r') as f:
|
|
f_lines = f.readlines()
|
|
|
|
f_full = patch_shader(fname, f_lines, game_settings_module, True)
|
|
|
|
with open(joinpath(OUT_DIR, fname), 'wb') as f:
|
|
f.write(f_full.encode('utf-8'))
|