#!/usr/bin/perl # # Ori and the Blind Forest DX9 data fixer # Written by Andrew Church use strict; use warnings; use MIME::Base64; $| = 1; if (!@ARGV || $ARGV[0] =~ /^-/) { die "\n" . "ori-fix.pl v0.1\n" . "Usage: $0 FILE.assets...\n" . "\n" . "Fixes missing DX9 shaders in \"Ori and the Blind Forest\" by\n" . "recompiling the corresponding DX11 shaders into DX9 code.\n" . "Run on all data files at once with \"perl ori-fix.pl *.assets\"\n" . "WARNING: Use at your own risk, and back up the game data first!\n" . "\n" . "Currently only supports pixel shaders in sharedassets[01].assets;\n" . "errors will be reported for other shaders.\n" . "\n" } # See https://code.google.com/p/fxdis-d3d1x/ my $fxdis_exe = &write_fxdis_exe; END { unlink $fxdis_exe if $fxdis_exe; } my $ok = 1; foreach my $assets_file (@ARGV) { print "Processing ${assets_file}...\n"; $ok &= eval {&process_assets_file($assets_file)} || 0; print "\033[1m$@\033[m" if $@; } if ($ok) { print "All files processed successfully.\n"; exit 0; } else { print "Some errors were encountered.\n"; exit 1; } sub process_assets_file { my ($assets_file) = @_; local *ASSETS; open ASSETS, "+<$assets_file" or die "$assets_file: $!\n"; binmode ASSETS; read ASSETS, $_, 20 or die "$assets_file: Unexpected EOF on header\n"; my ($metadata_size, $file_size, $version, $data_start, $big_endian) = unpack("NNNNC", $_); die "Unsupported version $version\n" if $version != 14; my $endian = $big_endian ? ">" : "<"; my $unity_version = ""; while (read(ASSETS,$_,1) && $_ ne "\0") { $unity_version .= $_; } read ASSETS, $_, 5 or die "$assets_file: Unexpected EOF on metadata header\n"; $_ eq "\5\0\0\0\0" or die "$assets_file: Unexpected data in metadata header\n"; my $shader_type = undef; my $type_count = &read_int(\*ASSETS, $endian); defined($type_count) or die "$assets_file: Unexpected EOF on type count\n"; my %types = (); for (my $i = 0; $i < $type_count; $i++) { my $id = &read_int(\*ASSETS, $endian, 1); defined($id) or die "$assets_file: Unexpected EOF on type $i ID\n"; my $guid; read ASSETS, $guid, 16 or die "$assets_file: Unexpected EOF on type $id GUID\n"; if ($id < 0) { my $guid2; read ASSETS, $guid2, 16 or die "$assets_file: Unexpected EOF on type $id second GUID\n"; } else { if (unpack("H*",$guid) eq "82cbe6ba9f2fd496d8e14747a764dc4f") { $shader_type = $id; } } } return 1 if !defined($shader_type); # No shaders in this file. my $num_files = &read_int(\*ASSETS, $endian); defined($num_files) or die "$assets_file: Unexpected EOF on file count\n"; if (tell(ASSETS) % 4) { read ASSETS, $_, 4 - (tell(ASSETS) % 4) or die "$assets_file: Unexpected EOF on file table padding\n"; } my $file_table_pos = tell(ASSETS); for (my $i = 0; $i < $num_files; $i++, $file_table_pos += 0x18) { seek ASSETS, $file_table_pos, 0 or die "$assets_file: Seek failed: $!\n"; read(ASSETS, $_, 0x18) == 0x18 or die "$assets_file: Unexpected EOF on file table entry $i\n"; my ($id, $zero, $offset, $size, $type, $unknown) = unpack("(LLLLlL)$endian", $_); $id == $i+1 or die "$assets_file: Wrong ID on file $i ($id, expected ".($i+1).")\n"; $zero == 0 or die "$assets_file: Wrong zero on file ID $id ($zero)\n"; if ($type == $shader_type) { seek ASSETS, $data_start + $offset, 0 or die "$assets_file: Seek failed: $!\n"; my $name = &read_string(\*ASSETS, $endian); my $length = &read_int(\*ASSETS, $endian); defined($length) or die "$assets_file: Unexpected EOF on file ID $id\n"; my $data; read(ASSETS, $data, $length) == $length or die "$assets_file: Unexpected EOF on file ID $id\n"; if (tell(ASSETS) % 4 != 0) { read ASSETS, $_, 4 - (tell(ASSETS) % 4); } my $trailer; my $trailer_len = $size - (tell(ASSETS) - ($data_start + $offset)); read(ASSETS, $trailer, $trailer_len) == $trailer_len or die "$assets_file: Unexpected EOF on file ID $id\n"; if ($data =~ /Platform d3d9 had shader errors/) { print " Fixing shader $name... "; my $new_data = eval {&rewrite_shader($data)}; if (!defined($new_data)) { print "\033[1m$@\033[m"; next; } seek ASSETS, 0, 2; my $new_offset = tell(ASSETS) - $data_start; my $header = pack("V", length($name)) . $name; $header .= "\0" x (4 - length($header)%4) if length($header)%4 != 0; $header .= pack("V", length($new_data)); $new_data .= "\0" x (4 - length($new_data)%4) if length($new_data)%4 != 0; $new_data = $header . $new_data . $trailer; print ASSETS $new_data or die "$assets_file: Write error on new shader: $!\n"; my $new_filesize = tell(ASSETS); seek ASSETS, 4, 0 or die "$assets_file: Seek failed: $!\n"; print ASSETS pack("N", $new_filesize) or die "$assets_file: Write error on file size update: $!\n"; seek ASSETS, $file_table_pos + 8, 0 or die "$assets_file: Seek failed: $!\n"; print ASSETS pack("VV", $new_offset, length($new_data)) or die "$assets_file: Write error on file table update: $!\n"; print "done.\n"; } } } close ASSETS; return 1; } sub read_int { local *F = $_[0]; my $endian = $_[1]; my $signed = $_[2]; my $data; read(F, $data, 4) == 4 or return undef; return unpack(($signed ? "l" : "L") . $endian, $data); } sub read_string { local *F = $_[0]; my $endian = $_[1]; my $length = &read_int(\*F, $endian); defined($length) or return undef; my $size = $length; $size += 4 - $length%4 if $length%4; my $data; read(F, $data, $size) == $size or return undef; return substr($data, 0, $length); } sub rewrite_shader { my ($data) = @_; my $search_for = 'Program "fp" { // Platform d3d9 had shader errors // SubProgram "opengl " { "!!GLSL" } SubProgram "d3d11 " { '; $data =~ s/^((?:.*\n)+)\Q$search_for\E((?:.*\n)*)\"ps_4_0\n([a-p\n]+)(\"(?:\s*\}){5})$/$1/ or die "Shader error located in unexpected place, can't fix\n"; my ($header, $code, $trailer) = ($2, $3, $4); my $d3d11 = $header . "\"ps_4_0\n" . $code . $trailer; $code =~ s/\n//g; $code =~ tr/a-p/0-9a-f/; my $tmpfile = "/tmp/ori-fix.$$"; local *F; open F, ">$tmpfile" or die "$tmpfile: $!\n"; print F pack("H*", $code); close F; my $d3d11_asm = `wine '$fxdis_exe' '$tmpfile' 2>/dev/null`; unlink $tmpfile; die "fxdis.exe failed\n" if $? != 0 || !$d3d11_asm; $d3d11_asm =~ s/\r//g; $d3d11_asm =~ s/\#.*\n//g; $d3d11_asm =~ s/^ps_4_0\n// or die "Unable to parse fxdis.exe output\n"; # There should be a vertex shader immediately before this. Parse # enough of it to figure out the output (varying) variable usages, # since ps_4_0 doesn't have usages on input declarations. my @input_usages; $data =~ /SubProgram "d3d9 " \{\n(?:[^\{]+\n)*"vs_3_0\n([^\"]+)"\n\}\nSubProgram "d3d11 " \{\n(?:[^\{]*\n)*"vs_4_0\n[a-p\n]+"\n\}\n\}\n$/ or die "Failed to find corresponding DX9 vertex shader\n"; my $d3d9_vs = $1; while ($d3d9_vs =~ s/^(.+)\n//) { local $_ = $1; if (/^dcl_((?:texcoord|color)\d*) o/) { # Ignore position outputs. push @input_usages, $1; } } my $d3d9_header = ""; my @cregs; my @cbmap; while ($header =~ s/^(.+)\n//) { local $_ = $1; if (/^SetTexture/) { $d3d9_header .= "$_\n"; } elsif (/^ConstBuffer/) { if (!/^ConstBuffer "\$Globals" \d+$/) { die "Unknown ConstBuffer line: $_\n"; } } elsif (/^BindCB/) { if ($_ ne 'BindCB "$Globals" 0') { die "Unknown BindCB line: $_\n"; } } elsif (/^Vector (\d+) \[(.+)\]$/) { my $offset = int($1/16); my $index = scalar(@cregs); $d3d9_header .= sprintf("Vector %d [%s]\n", $index, $2); push @cregs, [$offset*4, 4]; $cbmap[$offset] = [[$index,0], [$index,1], [$index,2], [$index,3]]; } elsif (/^Float (\d+) \[(.+)\]$/) { my $offset = int($1/4); my $index = scalar(@cregs); $d3d9_header .= sprintf("Float %d [%s]\n", $index, $2); push @cregs, [$offset, 1]; $cbmap[int($offset/4)] = [] if !$cbmap[int($offset/4)]; $cbmap[int($offset/4)][$offset%4] = [$index,0]; } else { die "Unknown header line: $_\n"; } } die "Junk at end of headers: $header\n" if $header; my $d3d9_asm = ""; my %samplers; my @float_constants; my @vec4_constants; my @float_constant_common; my @v_map; my $next_v = 0; my $first_free_reg = 0; while ($d3d11_asm =~ s/^(.+)(?:$|\n)//) { my $line = $1; if ($line =~ /^dcl_constant_buffer cb0\[\d+\]\.xyzw, immediateIndexed$/) { # Nothing needed. } elsif ($line =~ /^dcl_sampler sampler\[(\d+)\]$/) { $samplers{$1} = 1; } elsif ($line =~ /^dcl_resource_texture2d resource\[(\d+)\]$/) { die "Sampler not defined: $line\n" if !defined($samplers{$1}); delete $samplers{$1}; $d3d9_asm .= "dcl_2d s$1\n"; } elsif ($line =~ /^dcl_input_ps linear v(\d+)\.([xyzw]+)$/) { my $v = $next_v++; $v_map[$1] = $v; my $usage = $input_usages[$v] or die "Unknown usage for v$v: $line\n"; $d3d9_asm .= "dcl_$usage v$v.$2\n"; } elsif ($line =~ /^dcl_output o0\.xyzw$/) { # Nothing needed. } elsif ($line =~ /^dcl_temps \d+$/) { # Nothing needed. } elsif ($line eq "nop") { $d3d9_asm .= "nop\n"; } elsif ($line eq "ret") { $d3d9_asm .= "ret\n"; } else { if ($line !~ /^(\S+)(?: (.+))$/) { die "Unable to parse line: $line\n"; } my ($insn, $operands) = ($1, $2); $_ = ", $operands"; my @operands; while (s/^, (l\([^\)]+\)|[^,]+)//) { push @operands, $1; } die "Unable to parse operands: $line\n" if $_ ne ""; if ($operands[0] =~ /^r(\d+)/) { if ($1 >= $first_free_reg) { $first_free_reg = $1 + 1; } } if ($insn eq "sample" && $operands =~ /^r(\d+)\.([xyzw]+), ([vr])(\d+)\.([xyzw]+), resource\[(\d+)\].([xyzw]+), sampler\[(\d+)\]$/) { my ($out, $out_suffix, $in_class, $in, $in_suffix, $sampler, $sampler_suffix, $sampler2) = ($1, $2, $3, $4, $5, $6, $7, $8); die "Sampler index mismatch: $line\n" if $sampler2 != $sampler; if ($in_class eq "v") { defined($v_map[$in]) or die "Unexpected v$in: $line\n"; $in = $v_map[$in]; } $d3d9_asm .= "texld r$out.$out_suffix, $in_class$in.$in_suffix, s$sampler.$sampler_suffix\n"; } elsif ($insn eq "sample_l" && $operands =~ /^r(\d+)\.([xyzw]+), ([vr])(\d+)\.([xyzw]+), resource\[(\d+)\].([xyzw]+), sampler\[(\d+)\], l\(([\d+])\)$/) { my ($out, $out_suffix, $in_class, $in, $in_suffix, $sampler, $sampler_suffix, $sampler2, $lod) = ($1, $2, $3, $4, $5, $6, $7, $8, $9); if ($out >= $first_free_reg) { $first_free_reg = $out + 1; } die "Sampler index mismatch: $line\n" if $sampler2 != $sampler; if ($in_class eq "v") { defined($v_map[$in]) or die "Unexpected v$in: $line\n"; $in = $v_map[$in]; } if (!defined($float_constant_common[$lod])) { $float_constant_common[$lod] = scalar(@float_constants); push @float_constants, $lod; } my $fc_index = $float_constant_common[$lod]; $d3d9_asm .= "mov r$first_free_reg, $in_class$in\n"; $d3d9_asm .= "mov r$first_free_reg.w, \n"; $in_suffix = substr($in_suffix, 0, 2) . "zw"; $d3d9_asm .= "texldl r$out.$out_suffix, r$first_free_reg.$in_suffix, s$sampler.$sampler_suffix\n"; } elsif (($insn eq "rsq" || $insn eq "sqrt") && $operands =~ /r(\d+)\.([xyzw]+), ([vrc]\d+)\.([xyzw]+)$/) { my ($out, $out_suffix, $in, $in_suffix) = ($1, $2, $3, $4); $in_suffix .= substr($in_suffix, -1) while length($in_suffix) < 4; for (my $i = 0; $i < 4; $i++) { my $component = substr("xyzw", $i, 1); if ($out_suffix =~ /$component/) { $d3d9_asm .= "rsq r$out.$component, $in." . substr($in_suffix,$i,1) . "\n"; $d3d9_asm .= "rcp r$out.$component, r$out.$component\n" if $insn eq "sqrt"; } } } elsif ($insn =~ /^(add|dp[234]|deriv_rt[xy]|div|exp|frc|log|mad|max|min|mov|mul|sincos|sub)(_sat)?/) { my ($insn, $sat) = ($1, $2); $sat = $sat || ""; my @d3d9_ops; my $next_temp_reg = $first_free_reg; foreach my $op (@operands) { my $abs = 0; my $neg = 0; $abs = 1 if $op =~ s/^\|(.+)\|$/$1/; $neg = 1 if $op =~ s/^-(.+)$/$1/; my $d3d9_op; if ($op =~ /^l\(([^\)]+)\)$/) { my @literals = split(/, /, $1); if (@literals == 1) { my $index = scalar(@float_constants); push @float_constants, $literals[0]; $d3d9_op = ""; } else { die "Too many components in literal: $line\n" if @literals > 4; my $index = scalar(@vec4_constants); push @literals, 0 while @literals < 4; push @vec4_constants, [@literals]; $d3d9_op = ""; } } elsif ($op =~ /^cb0\[(\d+)\]\.([xyzw]+)$/) { my ($index, $suffix) = ($1, $2, $3); my $min_ofs = $index*4 + 3; my $max_ofs = $index*4; if ($suffix =~ /x/) { $min_ofs = $index*4; } if ($suffix =~ /y/) { $min_ofs = $index*4+1 if $min_ofs > $index*4+1; $max_ofs = $index*4+1; } if ($suffix =~ /z/) { $min_ofs = $index*4+2 if $min_ofs > $index*4+2; $max_ofs = $index*4+2; } if ($suffix =~ /w/) { $max_ofs = $index*4+2; } my $creg = $cbmap[$index][$min_ofs%4][0]; if ($min_ofs == $max_ofs) { $d3d9_op = "c$creg." . ("x" x length($suffix)) } else { # Must be a vector, so we can use the suffix unchanged. $d3d9_op = "c$creg.$suffix"; } } elsif ($op =~ /^([vro])(\d+)\.([xyzw]+)$/) { my ($class, $index, $suffix) = ($1, $2, $3); if ($class eq "v") { defined($v_map[$index]) or die "Unexpected v$index: $line\n"; $index = $v_map[$index]; } $class = "oC" if $class eq "o"; $d3d9_op = "$class$index.$suffix"; } else { die "Unable to parse operand $op: $line\n"; } if ($abs) { my $abs_suffix = ""; $abs_suffix .= "x" if $d3d9_op =~ /\.[xyzw]*x/; $abs_suffix .= "y" if $d3d9_op =~ /\.[xyzw]*y/; $abs_suffix .= "z" if $d3d9_op =~ /\.[xyzw]*z/; $abs_suffix .= "w" if $d3d9_op =~ /\.[xyzw]*w/; $abs_suffix = ".$abs_suffix" if $abs_suffix; $d3d9_asm .= "abs r$next_temp_reg$abs_suffix, $d3d9_op\n"; $d3d9_op =~ s/^[^.]+/r$next_temp_reg/; $next_temp_reg++; } if ($neg) { $d3d9_op = "-$d3d9_op"; } push @d3d9_ops, $d3d9_op; } # for each operand my $d3d9_insn = $insn; if ($insn =~ /^deriv_rt([xy])$/) { $d3d9_insn = "ds$1"; } elsif ($insn eq "div") { $d3d9_insn = "mul"; $d3d9_ops[-1] =~ /^([vrc]\d+)\.([xyzw]+)$/ or die "Unsupported second input to div: $line\n"; my ($reg, $suffix) = ($1, $2); $d3d9_asm .= "rcp r$next_temp_reg.x, $reg.x\n" if $suffix =~ /x/; $d3d9_asm .= "rcp r$next_temp_reg.y, $reg.y\n" if $suffix =~ /y/; $d3d9_asm .= "rcp r$next_temp_reg.z, $reg.z\n" if $suffix =~ /z/; $d3d9_asm .= "rcp r$next_temp_reg.w, $reg.w\n" if $suffix =~ /w/; $d3d9_ops[-1] = "r$next_temp_reg.$suffix"; $next_temp_reg++; } elsif ($insn eq "dp2") { if (!defined($float_constant_common[0])) { $float_constant_common[0] = scalar(@float_constants); push @float_constants, 0; } push @d3d9_ops, ""; } my $delayed_sat = 0; if ($sat && ($insn eq "frc" || $insn eq "sincos")) { $sat = ""; $delayed_sat = 1; } $d3d9_asm .= "$d3d9_insn$sat " . join(", ", @d3d9_ops) . "\n"; if ($delayed_sat) { $d3d9_asm .= "mov_sat $d3d9_ops[0], $d3d9_ops[0]\n"; } } else { die "Unhandled ps_4_0 instruction: $line\n"; } } } # Omit the final "ret" instruction, if any. $d3d9_asm =~ s/ret\n$//; my $d3d9_def = ""; my $c_index = scalar(@cregs); for (my $i = 0; $i < @vec4_constants; $i++) { my @constant = @{$vec4_constants[$i]}; map {s/^inf$/1e42/} @constant; $d3d9_def .= "def c$c_index, " . join(", ", @constant) . "\n"; $d3d9_asm =~ s//c$c_index/g; $c_index++; } for (my $i = 0; $i < @float_constants; $i += 4) { my @constant = (@float_constants, 0, 0, 0)[$i..$i+3]; map {s/^inf$/1e42/} @constant; $d3d9_def .= "def c$c_index, " . join(", ", @constant) . "\n"; for (my $j = $i; $j < $i+4; $j++) { my $suffix = substr("xyzw", $j-$i, 1); $d3d9_asm =~ s//c$c_index.$suffix/g; } $c_index++; } #$d3d9_asm="def c$c_index, 0, 1, 1, 1\nmov oC0, c$c_index\n"; #FIXME temp my $d3d9 = $d3d9_header . "\"ps_3_0\n" . $d3d9_def . $d3d9_asm . "\"\n"; #die $d3d9; #FIXME temp $data .= "Program \"fp\" {\n"; $data .= "SubProgram \"opengl \" {\n"; $data .= "\"!!GLSL\"\n"; $data .= "}\n"; $data .= "SubProgram \"d3d9 \" {\n"; $data .= $d3d9; $data .= "}\n"; $data .= "SubProgram \"d3d11 \" {\n"; $data .= $d3d11; return $data; } sub write_fxdis_exe { my $fxdis_exe = "/tmp/fxdis.exe"; my $fxdis_exe_gz = "${fxdis_exe}.gz"; local *F; open F, ">$fxdis_exe_gz" or die "$fxdis_exe_gz: $!\n"; my $data = <