amd-tools: consolidate patching and farc (un)packing into AFT Shader Patcher, and switch to xdelta vcdiff generation for use with new Novidia

This commit is contained in:
somewhatlurker
2020-11-13 00:03:54 +11:00
parent 9b1ab83b15
commit ddb25dab2d
14 changed files with 697 additions and 43 deletions
@@ -0,0 +1,103 @@
# gane-specific settings for diva arcade future tone
GAME_NAME = "Project DIVA Arcade: Future Tone"
from re import compile as recompile
# some skinning can't be done due to using NV parameter buffers, so use an otherwise unused texture instead.
VP_SKINNING_REGEX = recompile(r"INT TEMP _adr;\s*?(BUFFER4[^;]*?;)([\s\S]*?_mtxpal)")
VP_SKINNING_SUB = "TEMP _adr;\n#\\1 \nTEMP mtx_tmp, mtx_tmp1, mtx_tmp2;\\2"
# load the address temp with a texture coordinate instead
# hardcoded to assume a 768*1 pixel texture
VP_SKINNING_REGEX_2 = recompile(r"(BUFFER4[\s\S]*?)CVT(?:.S32.F32)?\s*?(.*?),\s*?(.*?);")
VP_SKINNING_SUB_2 = "\\1MUL \\2, \\3, 0.0013037809647979;" #1/767
VP_SKINNING_REGEX_3 = recompile(r"(BUFFER4[\s\S]*?)(DP4 ([^;]*?), _mtxpal\[([^\]]*?\.x) \])")
VP_SKINNING_REGEX_3b = recompile(r"(BUFFER4[\s\S]*?)(DP4 ([^;]*?), _mtxpal\[([^\]]*?\.y) \])")
VP_SKINNING_REGEX_3c = recompile(r"(BUFFER4[\s\S]*?)(DP4 ([^;]*?), _mtxpal\[([^\]]*?\.z) \])")
VP_SKINNING_REGEX_3d = recompile(r"(BUFFER4[\s\S]*?)(DP4 ([^;]*?), _mtxpal\[([^\]]*?\.w) \])")
VP_SKINNING_SUB_3 = "\\1TEX mtx_tmp, \\4, texture[8], 1D; ADD \\4, \\4, 0.0013037809647979; TEX mtx_tmp1, \\4, texture[8], 1D; ADD \\4, \\4, 0.0013037809647979; TEX mtx_tmp2, \\4, texture[8], 1D; DP4 \\3, _mtxpal[\\4+ ]"
VP_SKINNING_REGEX_4 = recompile(r"(BUFFER4[\s\S]*?)_mtxpal\[([^\]]*?\.\S)\+?(\s|\d)\]")
VP_SKINNING_SUB_4 = "\\1mtx_tmp\\3"
# some kind of effect (scrunching up or stretching?) in tights vp shaders starting with 11 that partially uses skinning of normals...
# works much the same as the regular vp skinning
# make these run before VP_SKINNING_REGEX_4
#TIGHTS_SKINNING_EFFECT_REGEX = recompile(r"(BUFFER4[\s\S]*?)SUBC .*?, vertex.attrib\[15\], .*?;\s*?SUBC1 tmp, vertex.attrib\[15\], .*?;[\s\S]*?XPD .*?, .*?, .*?;\s*?DP3_SAT (.*?), .*?, .*?;")
#TIGHTS_SKINNING_EFFECT_SUB = "\\1MOV \\2, 0;"
TIGHTS_SKINNING_EFFECT_REGEX = recompile(r"(BUFFER4[\s\S]*?)CVT(?:.S32.F32)?\s*?(.*?),\s*?(.*?);")
TIGHTS_SKINNING_EFFECT_SUB = "\\1MUL \\2, \\3, 0.0013037809647979;"
#TIGHTS_SKINNING_EFFECT_REGEX_2 = recompile(r"(BUFFER4[\s\S]*?)(IF GT1.y;[\s\S]*?|)(DP3 ([^;]*?), _mtxpal\[([^\]]*?\.x) \])(?![\s\S]*MUL _adr)")
#TIGHTS_SKINNING_EFFECT_REGEX_2b = recompile(r"(BUFFER4[\s\S]*?)(IF GT1.y;[\s\S]*?|)(DP3 ([^;]*?), _mtxpal\[([^\]]*?\.y) \])(?![\s\S]*MUL _adr)")
#TIGHTS_SKINNING_EFFECT_SUB_2 = "\\1TEX mtx_tmp, \\5, texture[8], 1D; ADD \\5, \\5, 0.0013037809647979; TEX mtx_tmp1, \\5, texture[8], 1D; ADD \\5, \\5, 0.0013037809647979; TEX mtx_tmp2, \\5, texture[8], 1D; \\2 DP3 \\4, _mtxpal[\\5+ ]"
TIGHTS_SKINNING_EFFECT_REGEX_2 = recompile(r"(BUFFER4[\s\S]*?)(DP3 ([^;]*?), _mtxpal\[([^\]]*?\.x) \])(?![\s\S]*MUL _adr)")
TIGHTS_SKINNING_EFFECT_SUB_2 = "\\1TEX mtx_tmp, \\4, texture[8], 1D; ADD \\4, \\4, 0.0013037809647979; TEX mtx_tmp1, \\4, texture[8], 1D; ADD \\4, \\4, 0.0013037809647979; TEX mtx_tmp2, \\4, texture[8], 1D; DP3 \\3, _mtxpal[\\4+ ]"
TIGHTS_SKINNING_EFFECT_REGEX_3 = recompile(r"(BUFFER4[\s\S]*?)(DP3 ([^;]*?), _mtxpal\[([^\]]*?\.y) \])(?![\s\S]*MUL _adr)")
TIGHTS_SKINNING_EFFECT_SUB_3 = "\\1TEX mtx_tmp, \\4, texture[8], 1D; DP3 \\3, _mtxpal[\\4+ ]"
TIGHTS_SKINNING_EFFECT_REGEX_4 = recompile(r"(BUFFER4[\s\S]*?)(DP3 ([^;]*?), _mtxpal\[([^\]]*?\.y)\+1\])(?![\s\S]*MUL _adr)")
TIGHTS_SKINNING_EFFECT_SUB_4 = "\\1ADD \\4, \\4, 0.0013037809647979; TEX mtx_tmp1, \\4, texture[8], 1D; ADD \\4, \\4, 0.0013037809647979; TEX mtx_tmp2, \\4, texture[8], 1D; DP3 \\3, _mtxpal[\\4+1]"
# some skinning just needs some branching fixup for AMD BRA working different (needs a target per branch instruction apparently)
SKIN_BRA_REGEX = recompile(r"BRA skinning_(?:\S*)end (\(.*?\.y\))*?\s*?;")
SKIN_BRA_SUB = "BRA sk_end1 \\1;"
SKIN_BRA_REGEX_2 = recompile(r"BRA skinning_(?:\S*)end (\(.*?\.z\))*?\s*?;")
SKIN_BRA_SUB_2 = "BRA sk_end2 \\1;"
SKIN_BRA_REGEX_3 = recompile(r"BRA skinning_(?:\S*)end (\(.*?\.w\))*?\s*?;")
SKIN_BRA_SUB_3 = "BRA sk_end3 \\1;"
SKIN_BRA_TGT_REGEX = recompile(r"skinning_(?:\S*)end:")
SKIN_BRA_TGT_SUB = "\nsk_end1:\nsk_end2:\nsk_end3:\n"
MATCH_FP_FNAME = recompile(r".*fp")
MATCH_VP_FNAME = recompile(r".*vp")
MATCH_ALL_FNAME = recompile(r".*")
MATCH_FONT_FNAME = recompile(r"font.*fp")
MATCH_TIGHTS_FNAME = recompile(r"tights.*vp")
# fourth param: apply continuously until no more matches are found
fix_repls = [
(MATCH_VP_FNAME, VP_SKINNING_REGEX, VP_SKINNING_SUB, False),
(MATCH_VP_FNAME, VP_SKINNING_REGEX_2, VP_SKINNING_SUB_2, False),
(MATCH_VP_FNAME, VP_SKINNING_REGEX_3, VP_SKINNING_SUB_3, False),
(MATCH_VP_FNAME, VP_SKINNING_REGEX_3b, VP_SKINNING_SUB_3, False),
(MATCH_VP_FNAME, VP_SKINNING_REGEX_3c, VP_SKINNING_SUB_3, False),
(MATCH_VP_FNAME, VP_SKINNING_REGEX_3d, VP_SKINNING_SUB_3, False),
(MATCH_TIGHTS_FNAME, TIGHTS_SKINNING_EFFECT_REGEX, TIGHTS_SKINNING_EFFECT_SUB, False),
(MATCH_TIGHTS_FNAME, TIGHTS_SKINNING_EFFECT_REGEX_2, TIGHTS_SKINNING_EFFECT_SUB_2, False),
(MATCH_TIGHTS_FNAME, TIGHTS_SKINNING_EFFECT_REGEX_3, TIGHTS_SKINNING_EFFECT_SUB_3, False),
(MATCH_TIGHTS_FNAME, TIGHTS_SKINNING_EFFECT_REGEX_4, TIGHTS_SKINNING_EFFECT_SUB_4, False),
(MATCH_VP_FNAME, VP_SKINNING_REGEX_4, VP_SKINNING_SUB_4, True),
(MATCH_VP_FNAME, SKIN_BRA_REGEX, SKIN_BRA_SUB, False),
(MATCH_VP_FNAME, SKIN_BRA_REGEX_2, SKIN_BRA_SUB_2, False),
(MATCH_VP_FNAME, SKIN_BRA_REGEX_3, SKIN_BRA_SUB_3, False),
(MATCH_VP_FNAME, SKIN_BRA_TGT_REGEX, SKIN_BRA_TGT_SUB, False),
]
# use to open a differnt file instead of the original shader (eg. to replace with an easier-to-patch variant
def filename_filter(fname):
if fname == 'sss_filter.130.fp':
return 'sss_filter.120.fp'
elif fname == 'sss_filter.131.fp':
return 'sss_filter.121.fp'
else:
return fname
# use to tweak reasults after patching
def post_filter(fname, f_full):
f_full = f_full.replace('\nBUFFER4', '\n#BUFFER4')
# fix tex sampler offsets
# diva has something we can use for pixel size (program.local[0]), but it isn't in fonts so they get an approximation instead
if MATCH_FONT_FNAME.match(fname):
f_full = f_full.replace('{ 0.00078, 0.0014 }; ADD tex_offset_coord', '0.00035; ADD tex_offset_coord')
else:
f_full = f_full.replace('{ 0.00078, 0.0014 }; ADD tex_offset_coord', 'program.local[0]; ADD tex_offset_coord')
return f_full
@@ -0,0 +1,74 @@
# gane-specific settings for diva arcade future tone
GAME_NAME = "Project DIVA Arcade: Future Tone"
from re import compile as recompile
# some skinning can't be done due to using NV parameter buffers, so just disable it.
VP_SKINNING_REGEX = recompile(r"(BUFFER4[\s\S]*?)MOV (.*?).w, 1; SUBC _tmp1, vertex.attrib\[15\], (-1|255); (.*?) IF NE.y; (.*?) MAD \2.xyz, _tmp0, vertex.attrib\[1\].y, \2; (.*?) MAD (.*?).xyz, _tmp0, vertex.attrib\[1\].y, \7; (.*?) MAD (.*?).xyz, _tmp0, vertex.attrib\[1\].y, \9; IF NE.z; (.*?) MAD \2.xyz, _tmp0, vertex.attrib\[1\].z, \2; (.*?) MAD \7.xyz, _tmp0, vertex.attrib\[1\].z, \7; (.*?) MAD \9.xyz, _tmp0, vertex.attrib\[1\].z, \9; IF NE.w; (.*?) MAD \2.xyz, _tmp0, vertex.attrib\[1\].w, \2; (.*?) MAD \7.xyz, _tmp0, vertex.attrib\[1\].w, \7; (.*?) MAD \9.xyz, _tmp0, vertex.attrib\[1\].w, \9; (?:ENDIF; ENDIF; ENDIF;|.*:)")
VP_SKINNING_SUB = "\\1MOV \\2.w, 1; MOV \\2.xyz, vertex.position; MOV \\7, vertex.normal; MOV \\9, vertex.attrib[6];"
VP_SKINNING_REGEX_2 = recompile(r"(BUFFER4[\s\S]*?)MOV (.*?).w, 1; SUBC _tmp1, vertex.attrib\[15\], (-1|255); (.*?) IF NE.y; (.*?) MAD \2.xyz, _tmp0, vertex.attrib\[1\].y, \2; (.*?) MAD (.*?).xyz, _tmp0, vertex.attrib\[1\].y, \7; IF NE.z; (.*?) MAD \2.xyz, _tmp0, vertex.attrib\[1\].z, \2; (.*?) MAD \7.xyz, _tmp0, vertex.attrib\[1\].z, \7; IF NE.w; (.*?) MAD \2.xyz, _tmp0, vertex.attrib\[1\].w, \2; (.*?) MAD \7.xyz, _tmp0, vertex.attrib\[1\].w, \7; (?:ENDIF; ENDIF; ENDIF;|.*:)")
VP_SKINNING_SUB_2 = "\\1MOV \\2.w, 1; MOV \\2.xyz, vertex.position; MOV \\7, vertex.normal;"
VP_SKINNING_REGEX_3 = recompile(r"(BUFFER4[\s\S]*?)MOV (.*?).w, 1; SUBC _tmp1, vertex.attrib\[15\], (-1|255); (.*?) IF NE.y; (.*?) MAD \2.xyz, _tmp0, vertex.attrib\[1\].y, \2; IF NE.z; (.*?) MAD \2.xyz, _tmp0, vertex.attrib\[1\].z, \2; IF NE.w; (.*?) MAD \2.xyz, _tmp0, vertex.attrib\[1\].w, \2; (?:ENDIF; ENDIF; ENDIF;|.*:)")
VP_SKINNING_SUB_3 = "\\1MOV \\2.w, 1; MOV \\2.xyz, vertex.position;"
# some kind of effect (scrunching up or stretching?) in tights shaders starting with 11 that partially uses skinning of normals... let's just pretend it always results in 0
TIGHTS_SKINNING_EFFECT_REGEX = recompile(r"(BUFFER4[\s\S]*?)SUBC .*?, vertex.attrib\[15\], .*?;\s*?SUBC1 tmp, vertex.attrib\[15\], .*?;[\s\S]*?XPD .*?, .*?, .*?;\s*?DP3_SAT (.*?), .*?, .*?;")
TIGHTS_SKINNING_EFFECT_SUB = "\\1MOV \\2, 0;"
# some skinning just needs some branching fixup for AMD BRA working different (needs a target per branch instruction apparently)
SKIN_BRA_REGEX = recompile(r"BRA skinning_(?:\S*)end (\(.*?\.y\))*?\s*?;")
SKIN_BRA_SUB = "BRA sk_end1 \\1;"
SKIN_BRA_REGEX_2 = recompile(r"BRA skinning_(?:\S*)end (\(.*?\.z\))*?\s*?;")
SKIN_BRA_SUB_2 = "BRA sk_end2 \\1;"
SKIN_BRA_REGEX_3 = recompile(r"BRA skinning_(?:\S*)end (\(.*?\.w\))*?\s*?;")
SKIN_BRA_SUB_3 = "BRA sk_end3 \\1;"
SKIN_BRA_TGT_REGEX = recompile(r"skinning_(?:\S*)end:")
SKIN_BRA_TGT_SUB = "\nsk_end1:\nsk_end2:\nsk_end3:\n"
MATCH_FP_FNAME = recompile(r".*fp")
MATCH_VP_FNAME = recompile(r".*vp")
MATCH_ALL_FNAME = recompile(r".*")
MATCH_FONT_FNAME = recompile(r"font.*fp")
MATCH_TIGHTS_FNAME = recompile(r"tights.*vp")
# fourth param: apply continuously until no more matches are found
fix_repls = [
(MATCH_VP_FNAME, VP_SKINNING_REGEX, VP_SKINNING_SUB, False),
(MATCH_VP_FNAME, VP_SKINNING_REGEX_2, VP_SKINNING_SUB_2, False),
(MATCH_VP_FNAME, VP_SKINNING_REGEX_3, VP_SKINNING_SUB_3, False),
(MATCH_TIGHTS_FNAME, TIGHTS_SKINNING_EFFECT_REGEX, TIGHTS_SKINNING_EFFECT_SUB, False),
(MATCH_VP_FNAME, SKIN_BRA_REGEX, SKIN_BRA_SUB, False),
(MATCH_VP_FNAME, SKIN_BRA_REGEX_2, SKIN_BRA_SUB_2, False),
(MATCH_VP_FNAME, SKIN_BRA_REGEX_3, SKIN_BRA_SUB_3, False),
(MATCH_VP_FNAME, SKIN_BRA_TGT_REGEX, SKIN_BRA_TGT_SUB, False),
]
# use to open a differnt file instead of the original shader (eg. to replace with an easier-to-patch variant
def filename_filter(fname):
if fname == 'sss_filter.130.fp':
return 'sss_filter.120.fp'
elif fname == 'sss_filter.131.fp':
return 'sss_filter.121.fp'
else:
return fname
# use to tweak reasults after patching
def post_filter(fname, f_full):
f_full = f_full.replace('\nBUFFER4', '\n#BUFFER4')
# fix tex sampler offsets
# diva has something we can use for pixel size (program.local[0]), but it isn't in fonts so they get an approximation instead
if MATCH_FONT_FNAME.match(fname):
f_full = f_full.replace('{ 0.00078, 0.0014 }; ADD tex_offset_coord', '0.00035; ADD tex_offset_coord')
else:
f_full = f_full.replace('{ 0.00078, 0.0014 }; ADD tex_offset_coord', 'program.local[0]; ADD tex_offset_coord')
return f_full
@@ -0,0 +1,240 @@
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'))
@@ -0,0 +1,80 @@
# regexes and config info is stored here to reduce clutter in main script
from re import compile as recompile
# inline these because... idk
# ATTRIB and OUTPUT must be inlined for vp tho
ATTRIB_REGEX = recompile(r"(?<!#)(?<!#\s)((SHORT\s*?|LONG\s*?)?ATTRIB\s*?([^\[\]\s]*?)\s*?=\s*?([^\s].*?);)\s*")
OUTPUT_REGEX = recompile(r"(?<!#)(?<!#\s)((SHORT\s*?|LONG\s*?)?OUTPUT\s*?([^\[\]\s]*?)\s*?=\s*?([^\s].*?);)\s*")
PARAM_REGEX = recompile(r"(?<!#)(?<!#\s)((SHORT\s*?|LONG\s*?)?PARAM\s*?([^\[\]\s]*?)\s*?=\s*?([^\s].*?);)\s*")
# help reduce param count more by commenting out unused ones that weren't inlined (array params can't be inlined)
UNUSED_PARAM_REGEX = recompile(r"(?<!#)(?<!#\s)((SHORT\s*?|LONG\s*?)?PARAM\s*?([^\[\]\s]*?)(\s*?\[.*?\])?\s*?=\s*?([^\s].*?);)\s*(?![\s\S]*[\s\-\+{]\3[\s,;\.\-\+\[])")
# I think some of these instructions might still be left after other patches.. they will be commented out
BAD_INSTR_REGEX = recompile(r"((?<!#)(?<!#\s)(CVT|BUFFER4|POW|NRM|REP)\s+?.*?;|ENDREP;|[^;\n]*\s*?(p_coef2d\[idx.x\]).*?;)\s*")
# find the header tag to replace with !!ARBxx1.0 and an NV OPTION
NVFP_HEADER_REGEX = recompile(r"!!NVfp\d\.\d")
NVVP_HEADER_REGEX = recompile(r"!!NVvp\d\.\d")
# POW, NRM, and SSG sometimes(?) don't work (depending on inputs?), so just reimplement with other instructions
POW_REGEX = recompile(r"POW(\s+?|_SAT\s+?)(.*?),\s+?(.*?),\s+?(.*?);")
POW_SUB = "LG2 \\2, \\3; MUL \\2, \\2, \\4; EX2\\1\\2, \\2;"
NRM_REGEX = recompile(r"NRM\s+?(.*?),\s+?(.*?);")
NRM_SUB = "DP3 nrm_tmp.w, \\2, \\2; RSQ nrm_tmp.w, nrm_tmp.w; MUL \\1, nrm_tmp.w, \\2;"
SSG_REGEX = recompile(r"SSG\s+?(\S.*?\.\S*?),\s+?(.*?);")
SSG_SUB = "SGE ssg_tmp1, \\2, 0; SUB ssg_tmp1, ssg_tmp1, 1; SGT ssg_tmp2, \\2, 0; ADD \\1, ssg_tmp1, ssg_tmp2;"
# seems like using a TEX with pixel offsets is pretty common, but I can't make that work
# afaik there's no general way to get texture resolution so I'll approximate one here that can be refined later per-shader
# ({ 0.00078, 0.0014 } is approximately one pixel of a 720p texture/framebuffer)
SAMPLER_OFFSET_REGEX = recompile(r"TEX\s+?([^;,]*?),\s+?([^;,]*?),\s+?([^;,]*?),\s+?2D,\s+?\((.*?)\);")
SAMPLER_OFFSET_SUB = "MUL tex_offset_coord, {\\4}, { 0.00078, 0.0014 }; ADD tex_offset_coord, \\2, tex_offset_coord; TEX \\1, tex_offset_coord, \\3, 2D;"
# ret at the end of an IF statement is moved to outside of it (or won't compile)
# not a perfect solution, but works well enough
RET_IF_REGEX = recompile(r"(IF\s+?(.*?);[\s\S]*?)RET;\s*?ENDIF;")
RET_IF_SUB = "\\1ENDIF;\nRET (\\2);"
# RET behaves differently on AMD, so convert conditional rets into if/else
# (may not work properly with CAL)
CONDITIONAL_RET_REGEX = recompile(r"RET\s+?\((.*?)\);([\s\S]*?)(END\s*?$|RET;)")
CONDITIONAL_RET_SUB = "IF \\1; ELSE; \\2 ENDIF; \\3"
# CAL behaves differently on AMD, so convert conditional cals into if
CONDITIONAL_CAL_REGEX = recompile(r"CAL\s+?(.*?)\s+?\((.*?)\)\s*?;")
CONDITIONAL_CAL_SUB = "IF \\2; CAL \\1; ENDIF;"
# seems like some suffixes (eg .F, .CC1) don't work
# first regex: drop everything before the CC suffix, and convert CC suffix to appended C0/C1
INSTR_SUFFIX_REGEX = recompile(r"(;\s*?)([A-Z\d]+)[A-Z\d\.]*?\.CC(\d)")
INSTR_SUFFIX_SUB = "\\1\\2C\\3"
# second regex: remove all remaining suffixes (or all suffixes for instructions without CC)
INSTR_SUFFIX_REGEX_2 = recompile(r"(;\s*?)([A-Z\d]+)\.[A-Z\.\d]+")
INSTR_SUFFIX_SUB_2 = "\\1\\2"
MATCH_FP_FNAME = recompile(r".*fp")
MATCH_VP_FNAME = recompile(r".*vp")
MATCH_ALL_FNAME = recompile(r".*")
# fourth param: apply continuously until no more matches are found
fix_repls = [
(MATCH_ALL_FNAME, POW_REGEX, POW_SUB, False),
(MATCH_ALL_FNAME, NRM_REGEX, NRM_SUB, False),
(MATCH_ALL_FNAME, SSG_REGEX, SSG_SUB, False),
(MATCH_ALL_FNAME, SAMPLER_OFFSET_REGEX, SAMPLER_OFFSET_SUB, False),
(MATCH_ALL_FNAME, RET_IF_REGEX, RET_IF_SUB, False),
(MATCH_ALL_FNAME, CONDITIONAL_RET_REGEX, CONDITIONAL_RET_SUB, True),
(MATCH_ALL_FNAME, CONDITIONAL_CAL_REGEX, CONDITIONAL_CAL_SUB, False),
(MATCH_ALL_FNAME, INSTR_SUFFIX_REGEX, INSTR_SUFFIX_SUB, False),
(MATCH_ALL_FNAME, INSTR_SUFFIX_REGEX_2, INSTR_SUFFIX_SUB_2, False),
]
@@ -0,0 +1,117 @@
import sys
from os import chdir, makedirs, get_terminal_size
from os.path import join as joinpath, splitext, isfile, exists, dirname, abspath
import pyfarc
import importlib, importlib.util
if getattr(sys, 'frozen', False):
datadir = dirname(sys.executable)
else:
datadir = dirname(__file__)
arbpatcher_dir = joinpath(datadir, 'ARB Patcher')
arbpatcher_gamesettings_dir = joinpath(arbpatcher_dir, 'gamesettings')
sys.path = [arbpatcher_dir] + sys.path
import main as ArbPatcher
sys.path = sys.path[1:]
game_settings_spec = importlib.util.spec_from_file_location('divaaft', joinpath(arbpatcher_gamesettings_dir, 'divaaft.py'))
game_settings_module = importlib.util.module_from_spec(game_settings_spec)
game_settings_spec.loader.exec_module(game_settings_module)
# not bothering to check for main here because it's just a glue script anyway
def get_args():
import argparse
parser = argparse.ArgumentParser(description='AFT Shader Patcher: Patches Nvidia-only ARB shaders from PDAFT to work on AMD. Patching techniques from Nezarn; implementation by somewhatlurker.')
parser.add_argument('-i', '--in_farc', default='shader.farc', help='input shader farc file (default: "shader.farc")')
parser.add_argument('-o', '--out_farc', default='shader_patched.farc', help='output shader farc file (default: "shader_patched.farc")')
parser.add_argument('-c', '--compress', action='store_true', help='force use of farc compression (this mey produce larger vcdiff files)')
parser.add_argument('-x', '--xdelta', action='store_true', help='generate a vcdiff patch file')
return parser.parse_args()
args = get_args()
print("AFT Shader Patcher")
print("==================")
print("Patching techniques from Nezarn; implementation by somewhatlurker")
print("=================================================================")
print("Input file: '{}'".format(args.in_farc))
print("Output file: '{}'".format(args.out_farc))
if not exists(args.in_farc):
print ("'{}' does not exist. Aborting".format(args.in_farc))
sys.exit()
elif not isfile(args.in_farc):
print ("'{}' is not a file. Aborting".format(args.in_farc))
sys.exit()
if exists(args.out_farc):
if not isfile(args.out_farc):
print ("'{}' already exists but is a directory. Aborting".format(args.out_farc))
sys.exit()
with open(args.in_farc, 'rb') as f:
farcdata = pyfarc.from_stream(f)
if args.xdelta:
f.seek(0)
from binascii import crc32
in_farc_crc_str = '{:08x}'.format(crc32(f.read()))
proc_count = 0
last_status_len = 0
for fname in farcdata['files']:
proc_count += 1
progress_val = proc_count / len(farcdata['files'])
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
f_lines = farcdata['files'][openname]['data'].decode('utf-8').splitlines(keepends=True)
f_full = ArbPatcher.patch_shader(fname, f_lines, game_settings_module, True)
farcdata['files'][fname]['data'] = f_full.encode('utf-8')
if args.compress:
farcdata['farc_type'] = 'FArC'
with open(args.out_farc, 'wb') as f:
pyfarc.to_stream(farcdata, f, no_copy=True)
if args.xdelta:
import subprocess
# make paths absolute before running this
#args.in_farc = abspath(args.in_farc)
#args.out_farc = abspath(args.out_farc)
subprocess.run([joinpath(datadir, 'xdelta3.exe'), '-e', '-f', '-S', 'none', '-s', args.in_farc, args.out_farc, joinpath(dirname(args.out_farc), in_farc_crc_str + '.vcdiff')], )
@@ -0,0 +1,252 @@
"""
pyfarc reader and writer for farc archives
supports Farc and FarC only
"""
from construct import Struct, Const, Int32ub, Int32sb, RepeatUntil, CString, Pointer, Bytes, Padding
from copy import deepcopy
import gzip
_FArc_format = Struct(
"signature" / Const(b'FArc'),
"header_size" / Int32ub, # doesn't include signature or header_size
"alignment" / Int32sb,
"files" / RepeatUntil(lambda obj,lst,ctx: ctx._io.tell() - 7 > ctx.header_size, Struct(
"name" / CString("utf8"),
"pointer" / Int32ub,
"size" / Int32ub,
"data" / Pointer(lambda this: this.pointer, Bytes(lambda this: this.size))
)),
#Padding(lambda this: this.alignment - (this._io.tell() % this.alignment) if this._io.tell() % this.alignment else 0)
)
_FArC_format = Struct(
"signature" / Const(b'FArC'),
"header_size" / Int32ub, # doesn't include signature or header_size
"alignment" / Int32sb,
"files" / RepeatUntil(lambda obj,lst,ctx: ctx._io.tell() - 7 > ctx.header_size, Struct(
"name" / CString("utf8"),
"pointer" / Int32ub,
"compressed_size" / Int32ub,
"uncompressed_size" / Int32ub,
"data" / Pointer(lambda this: this.pointer, Bytes(lambda this: this.compressed_size))
)),
#Padding(lambda this: this.alignment - (this._io.tell() % this.alignment) if this._io.tell() % this.alignment else 0)
)
_farc_types = {
'FArc': {
'remarks': 'basic farc format',
'struct': _FArc_format,
'compression_support': False,
'compression_forced': False,
'fixed_header_size': 4,
'files_header_fields_size': 8,
},
'FArC': {
'remarks': 'farc with compression support',
'struct': _FArC_format,
'compression_support': True,
'compression_forced': True,
'fixed_header_size': 4,
'files_header_fields_size': 12,
},
}
class UnsupportedFarcTypeException(Exception):
pass
def check_farc_type(t):
"""Checks if a farc type is supported and returns a remarks string. Raises UnsupportedFarcTypeException if not supported."""
if not t in _farc_types:
raise UnsupportedFarcTypeException("{} type not supported".format(t))
return _farc_types[t]['remarks']
def _files_header_size_calc(files, farc_type):
"""Sums the size of the files header section for the given files and farc_type data."""
size = 0
for fname, info in files.items():
size += len(fname) + 1
size += farc_type['files_header_fields_size']
return size
def _prep_files(files, alignment, farc_type):
"""Gets files ready for writing by compressing them and calculating pointers."""
def _compress_files(files, farc_type):
for fname, info in files.items():
info['data_compressed'] = gzip.compress(info['data'], mtime=39) # set mtime for reproducible output
if (not farc_type['compression_forced']) and (len(info['data_compressed']) >= len(info['data'])):
info['data_compressed'] = info['data']
def _set_files_pointers(files, alignment, farc_type):
pos = 8 + farc_type['fixed_header_size'] + _files_header_size_calc(files, farc_type)
for fname, info in files.items():
if pos % alignment: pos += alignment - (pos % alignment)
info['pointer'] = pos
if 'data_compressed' in info:
pos += len(info['data_compressed'])
else:
pos += len(info['data'])
if farc_type['compression_support']:
_compress_files(files, farc_type)
_set_files_pointers(files, alignment, farc_type)
def to_bytes(data, alignment=1, no_copy=False):
"""
Converts a farc dictionary (formatted like the dictionary returned by from_bytes) to an in-memory bytes object containing farc data.
Set no_copy to True for a speedup and memory usage reduction if you don't mind your input data being contaminated.
"""
magic_str = data['farc_type']
check_farc_type(magic_str)
farc_type = _farc_types[magic_str]
if no_copy:
files = data['files']
else:
files = deepcopy(data['files'])
_prep_files(files, alignment, farc_type)
if farc_type['compression_support']:
return farc_type['struct'].build(dict(
header_size=farc_type['fixed_header_size'] + _files_header_size_calc(files, farc_type),
alignment=alignment,
files=[dict(
name=fname,
pointer=info['pointer'],
compressed_size=len(info['data_compressed']),
uncompressed_size=len(info['data']),
data=info['data_compressed']
) for fname, info in files.items()]
))
else:
return farc_type['struct'].build(dict(
header_size=farc_type['fixed_header_size'] + _files_header_size_calc(files, farc_type),
alignment=alignment,
files=[dict(
name=fname,
pointer=info['pointer'],
size=len(info['data']),
data=info['data']
) for fname, info in files.items()]
))
def to_stream(data, stream, alignment=1, no_copy=False):
"""
Converts a farc dictionary (formatted like the dictionary returned by from_stream) to farc data and writes it to a stream.
Set no_copy to True for a speedup and memory usage reduction if you don't mind your input data being contaminated.
"""
magic_str = data['farc_type']
check_farc_type(magic_str)
farc_type = _farc_types[magic_str]
if no_copy:
files = data['files']
else:
files = deepcopy(data['files'])
_prep_files(files, alignment, farc_type)
if farc_type['compression_support']:
return farc_type['struct'].build_stream(dict(
header_size=farc_type['fixed_header_size'] + _files_header_size_calc(files, farc_type),
alignment=alignment,
files=[dict(
name=fname,
pointer=info['pointer'],
compressed_size=len(info['data_compressed']),
uncompressed_size=len(info['data']),
data=info['data_compressed']
) for fname, info in files.items()]
), stream)
else:
return farc_type['struct'].build_stream(dict(
header_size=farc_type['fixed_header_size'] + _files_header_size_calc(files, farc_type),
alignment=alignment,
files=[dict(
name=fname,
pointer=info['pointer'],
size=len(info['data']),
data=info['data']
) for fname, info in files.items()]
), stream)
def _parsed_to_dict(farcdata, farc_type):
"""Converts the raw construct data to our standard dictionary format."""
files = {}
if farc_type['compression_support']:
for f in farcdata['files']:
if farc_type['compression_forced'] or (f['uncompressed_size'] != f['compressed_size']):
data = gzip.decompress(f['data'])
else:
data = f['data']
files[f['name']] = {'data': data}
else:
for f in farcdata['files']:
data = f['data']
files[f['name']] = {'data': data}
return {'farc_type': farcdata['signature'].decode('ascii'), 'files': files}
def from_bytes(b):
"""Converts farc data from bytes to a dictionary."""
magic_str = b[:4].decode('ascii')
check_farc_type(magic_str)
farc_type = _farc_types[magic_str]
farcdata = farc_type['struct'].parse(b)
return _parsed_to_dict(farcdata, farc_type)
def from_stream(s):
"""Converts farc data from a stream to a dictionary."""
pos = s.tell()
magic_str = s.read(4).decode('ascii')
check_farc_type(magic_str)
farc_type = _farc_types[magic_str]
s.seek(pos)
farcdata = farc_type['struct'].parse_stream(s)
return _parsed_to_dict(farcdata, farc_type)
#test_farc = {'farc_type': 'FArc', 'files': {'aaa': {'data': b'test1'}, 'bbb': {'data': b'test2'}, 'ccc': {'data': b'aaaaaaaaaaaaaaaaaaaaaaaa'}}}
test_farc = {'farc_type': 'FArC', 'files': {'aaa': {'data': b'test1'}, 'bbb': {'data': b'test2'}, 'ccc': {'data': b'aaaaaaaaaaaaaaaaaaaaaaaa'}}}
#print (test_farc)
#test_bytes = to_bytes(test_farc, alignment=16)
#print (test_bytes)
#print (from_bytes(test_bytes))
#with open('test.farc', 'wb') as f:
# to_stream(test_farc, f, alignment=16)
#with open('test.farc', 'rb') as f:
# print (from_stream(f))
#with open('shader_amd.farc', 'rb') as f:
# shaderfarc = from_stream(f)
#with open('shader_amd_out.farc', 'wb') as f:
# to_stream(shaderfarc, f, alignment=16, no_copy=True)
#with open('shader_amd_compressed.farc', 'rb') as f:
# shaderfarc = from_stream(f)
#with open('shader_amd_out_compressed.farc', 'wb') as f:
# to_stream(shaderfarc, f, alignment=1, no_copy=True)
#with open('fontmap.farc', 'rb') as f:
# fontmapfarc = from_stream(f)
#with open('fontmap_out.farc', 'wb') as f:
# to_stream(fontmapfarc, f, alignment=1, no_copy=True)