Files
2013-04-25 13:56:18 -07:00

731 lines
22 KiB
ActionScript
Executable File

// ================================================================================
//
// ADOBE SYSTEMS INCORPORATED
// Copyright 2011 Adobe Systems Incorporated
// All Rights Reserved.
//
// NOTICE: Adobe permits you to use, modify, and distribute this file
// in accordance with the terms of the license agreement accompanying it.
//
// ================================================================================
package com.adobe.utils
{
// ===========================================================================
// Imports
// ---------------------------------------------------------------------------
//import flash.display3D.*;
import com.adobe.utils.macro.AGALPreAssembler;
import com.adobe.utils.macro.AGALVar;
import flash.display.Stage;
import flash.utils.*;
/**
* AGALMacroAssembler
* <p>A richer version of the AGALMiniAssembler. The MacroAssembler is
* a macro based assembly substitution "compiler". Basically it adds
* some nice, more human readable syntax to the MiniAssembler.</p>
*
* <p>It never generates temporaries and doesn't have an AST. (Use PixelBender for that.)</p>
*
* <p>Features:</p>
* <ul>
* <li>Math expressions on float4: addition, subtraction, multiplication, division, negation.
* Note that expression must be simple operations.</li>
<pre>
a = b ~~ c; // Simple binary operation
a = -a; // Simple unary operation
a = b~~c + d; // Bonus: multiply-add is supported
a = b~~c + d~~c; // ERROR! this isn't simple enough. (3 binary operations)
</pre>
* <li>Macro definitions. Like functions, but are actually substitutions
* into your code. The actual operation is very
* similar to the C preprocessor. The Macros are processed
* ahead of parsing, so order order doesn't matter, and A() can call B() as well as B() calling A().
* Recursion is not supported (or possible.).</li>
<pre>
macro Foo( arg0 ) {
out = arg0; // 'out' is always the return value.
}
</pre>
* <li> Aliases. Very handy for keeping track of your attribute and constant registers. It's
* a good idea to start any AGALMacro program with an alias block, that defines the mapping from
* the shader to the constant pool and vertex stream. </li>
<pre>
alias va0, pos
alias vc0, objToClip
op = MultMat4x4( pos, objToClip );
</pre>
* <li> Constant use. All constants must be in the constant pool. However, if you tell
* the macro assembler about your constants, it can use them automatically. You can also
* query what constants are expected by a shader. The 'aliases' member contains information
* about all the aliases and constants used by the shader.</li>
<pre>
alias vc0, CONST_0( 0.5, 1 ); // declares that vc0.x=0.5 and vc0.y=1. This must be true in your constant pool.
macro Half( arg0 ) {
out = arg0 ~~ 0.5; // will automatically use vc0.x, since you declared this to be 0.5
}
</pre>
* <li> Pre-processor. Sometimes you will one a shader to have multiple
* flavors. For example:
<pre>
ft0 = tex<2d,linear,miplinear>( v0, fs0 );
#if USE_ALPHA;
ft0 = ft0 * fc_alpha;
#endif;
#if USE_COLOR_XFORM;
ft0 = ft0 + fc_colorXForm;
#endif;
oc = ft0;
</pre>
Can be configured 4 ways with USE_ALPHA and USE_COLOR_XFORM on and off. You can easily
compile an minimal, efficient shader with just alpha support by saying:
<pre>
macroAssembler.assemble( "USE_ALPHA=1;" + SHADER_SRC_STRING );
</pre>
The preprocessor supports the following operations:
<pre>
#define FOO num
#define FOO
#define FOO=<expression>
#undef FOO
#if <expression>
#elif <expression>
#else
#endif
</pre>
Note that the preprocessor does NOT (currently) support the notions of "is defined". A variable has a value.
The expression parser is rich and follows the c precedence rules.
<pre>
#if VAR_ONE*VAR_TWO+VAR_THREE != VAR_ONE+VAR_THREE*VAR_TWO
...
#endif
</pre>
* </li>
<li>Simple standard library. The following macros are always available:
<pre>
ft0 = tex<2d,linear,miplinear>( v0, fs0 ); Function syntax for textures.
ft0 = mul3x3( vec, mat ) Matrix multiplication
ft0 = mul4x4( vec, mat ) Matrix multiplication
</pre>
</li>
* </ul>
*/
public class AGALMacroAssembler extends AGALMiniAssembler
{
// ======================================================================
// Properties
// ----------------------------------------------------------------------
/** The generated AGAL Mini Assembler code. Available after the call to assemble().*/
public var asmCode:String = "";
private var isFrag:Boolean = false;
public var profile:Boolean = false;
public var profileTrace:String = "";
private var stages:Vector.<PerfStage> = null;
// ======================================================================
// Constructor
// ----------------------------------------------------------------------
public function AGALMacroAssembler( debugging:Boolean = false ):void
{
super( debugging );
}
// ======================================================================
// Methods
// ----------------------------------------------------------------------
private static const REGEXP_LINE_BREAKER:RegExp = /[\f\n\r\v;]+/g;
private static const COMMENT:RegExp = /\/\/[^\n]*\n/g
private var macros:Dictionary = new Dictionary();
/** After assemble() is called, the aliases contain
* information about all the variables used in the
* shader. Of particular important is the constant
* registers that are required to be set for the
* shader to function. Each alias is of type AGALVar.
* A constant, required to be set, will have agalVar.isConstant() == true
*/
public var aliases:Dictionary = new Dictionary();
private var tokens:Vector.<String> = null;
private var types:String = "";
private var preproc:AGALPreAssembler = new AGALPreAssembler();
// Takes a string input, normalizes newlines, and removes comments.
private function cleanInput( source:String ):String
{
// Pull out the c-style comments first.
var start:int = source.indexOf( "/*" );
while( start >= 0 ) {
var end:int = source.indexOf( "*/", start+1 );
if ( end < 0 ) throw new Error( "Comment end not found." );
source = source.substr( 0, start ) + source.substr( end+2 );
start = source.indexOf( "/*" );
}
source = source.replace( REGEXP_LINE_BREAKER, "\n" );
source = source.replace( COMMENT, "" );
return source;
}
public static const IDENTIFIER:RegExp = /((2d)|(3d)|[_a-zA-Z])+([_a-zA-Z0-9.]*)/
public static const NUMBER:RegExp = /[0-9]+(?:\.[0-9]*)?/
// Nasty regex, even by regex standards: 2 char ops 1 char ops
public static const OPERATOR:RegExp = /(==)|(!=)|(<=)|(>=)|(&&)|(\|\|)|[*=+-\/()\[\]{}!<>&|]/;
public static const SEPERATOR:RegExp = /\n/
public static const PREPROC:RegExp = /\#[a-z]+/ // # (space) identifier
public static const TOKEN:RegExp = new RegExp( IDENTIFIER.source+"|"+
NUMBER.source+"|"+
SEPERATOR.source+"|"+
OPERATOR.source+"|"+
PREPROC.source,
"g" );
private static const MACRO:RegExp = /([\w.]+)(\s*)=(\s*)(\w+)(\s*)\(/
/* @internal */
public static function joinTokens( tokens:Vector.<String> ):String
{
var pos:int = 0;
var newline:int = 0;
var s:String = "";
var tokensLength:uint = tokens.length;
while ( pos < tokensLength ) {
if ( tokens[pos] == "\n" ) {
++pos;
continue;
}
newline = tokens.indexOf( "\n", pos+1 );
if ( newline < 0 ) newline = tokensLength;
s += tokens[pos++];
if ( pos < tokensLength && tokens[pos] != "." )
s += " ";
while ( pos < newline ) {
s += tokens[pos];
if ( tokens[pos] == "," )
s += " ";
++pos;
}
s += "\n";
pos = newline + 1;
}
return s;
}
private function tokenize( input:String ):Vector.<String>
{
// Tokens:
// identifiers: /w+
// operators: =+-/*
return Vector.<String>( input.match( TOKEN ) );
}
private function tokenizeTypes( tokens:Vector.<String> ):String
{
var types:String = "";
var tokensLength:uint = tokens.length;
for( var i:uint=0; i<tokensLength; ++i ) {
var token:String = tokens[i];
if ( token.search( IDENTIFIER ) == 0 ) {
types += "i";
}
else if ( token.search( NUMBER ) == 0 ) {
types += "0";
}
else if ( token.search( SEPERATOR ) == 0 ) {
types += "n";
}
else if ( token.search( OPERATOR ) == 0) {
if ( token.length == 1 )
types += token;
else
types += "2"; // ==, !=, etc.
}
else if ( token.search( PREPROC ) == 0 ) {
types += "#";
}
else {
throw new Error( "Unrecognized token: " + tokens[i] );
}
}
if ( types.length != tokens.length )
throw new Error( "Tokens and types must have the same length." );
return types;
}
private function createMangledName( name:String, types:int ):String
{
// All we really need is the nArgs, since that is all that can change.
return name + "-" + types;
}
private var emptyStringVector:Vector.<String> = new Vector.<String>( 0, true );
private function splice( pos:int, deleteCount:int, newTokens:Vector.<String>, newTypes:String ):void
{
if ( newTokens == null )
newTokens = emptyStringVector;
if ( newTypes == null )
newTypes = "";
var t:Vector.<String> = tokens.slice( 0, pos );
t = t.concat( newTokens );
t = t.concat( tokens.slice( pos+deleteCount ) );
tokens = t;
types = types.substr( 0, pos ) + newTypes + types.substr( pos+deleteCount );
if ( types.length != tokens.length )
throw new Error( "AGAL.splice internal error. types.length=" + types.length + " tokens.length=" + tokens.length );
}
private function basicOp( op:String, target:String, src1:String, src2:String ):String
{
return op + " " + target + ", " + src1 + ", " + src2;
}
private function convertMath( pos:int ):Boolean
{
// dest = rega
// dest = rega + regb
// dest = rega - regb
// dest = rega / regb
// dest = rega * regb
// dest = -rega
var end:int = types.indexOf( "n", pos+1 );
if ( end < pos+1 )
throw new Error( "End of expression not found." );
var body:String = "";
var s:String = types.substr( pos, end-pos );
switch( s ) {
case "i=i":
body = "mov "+tokens[pos+0]+", "+tokens[pos+2];
break;
case "i=i+i":
body = basicOp( "add", tokens[pos+0], tokens[pos+2], tokens[pos+4] )
break;
case "i=i-i":
body = basicOp( "sub", tokens[pos+0], tokens[pos+2], tokens[pos+4] )
break;
case "i=i*i":
body = basicOp( "mul", tokens[pos+0], tokens[pos+2], tokens[pos+4] )
break;
case "i=i/i":
body = basicOp( "div", tokens[pos+0], tokens[pos+2], tokens[pos+4] )
break;
case "i=-i":
body = "neg "+tokens[pos+0]+", "+tokens[pos+3];
break;
case "i*=i":
body = basicOp( "mul", tokens[pos+0], tokens[pos+0], tokens[pos+3] );
break;
case "i/=i":
body = basicOp( "div", tokens[pos+0], tokens[pos+0], tokens[pos+3] );
break;
case "i+=i":
body = basicOp( "add", tokens[pos+0], tokens[pos+0], tokens[pos+3] );
break;
case "i-=i":
body = basicOp( "sub", tokens[pos+0], tokens[pos+0], tokens[pos+3] );
break;
case "i=i*i+i":
body = basicOp( "mul", tokens[pos+0], tokens[pos+2], tokens[pos+4] ) + "\n"
+ basicOp( "add", tokens[pos+0], tokens[pos+0], tokens[pos+6] )
break;
case "i=i+i*i":
body = basicOp( "mul", tokens[pos+0], tokens[pos+4], tokens[pos+6] ) + "\n"
+ basicOp( "add", tokens[pos+0], tokens[pos+0], tokens[pos+2] )
break;
default:
return false;
}
if ( body.length > 0 ) {
var tok:Vector.<String> = tokenize( body );
var typ:String = tokenizeTypes( tok );
splice( pos, end-pos, tok, typ );
}
return true;
}
// Add the macros to the internal table.
// Returns a string without the macros.
private function processMacro( pos:int ):void
{
// function Foo( arg0, arg1, arg2, ... )
var NAME:int = 1;
var OPENPAREN:int = 2;
var ARG0:int = 3;
var openParen:int = 0;
var closeParen:int = 0;
var openBracket:int = 0;
var closeBracket:int = 0;
var i:int = 0;
if ( tokens[pos] != "macro" ) throw new Error( "Expected 'macro' not found." );
openParen = pos+OPENPAREN;
if ( tokens[openParen] != "(" )
throw new Error( "Macro open paren not found." );
closeParen = types.indexOf( ")", openParen+1 );
openBracket = types.indexOf( "{", closeParen+1 );
closeBracket = types.indexOf( "}", openBracket+1 );
var macro:Macro = new Macro(); // name, args, body
macro.name = tokens[pos+NAME];
// normally: (a, b, c)
// but also: (a, b, <c,d>)
var argc:int = 0;
for( i=openParen+1; i<closeParen; ++i ) {
if ( types.charAt( i ) == "i" ) {
macro.args.push( tokens[i] );
++argc;
}
}
macro.mangledName = createMangledName( macro.name, argc );
// Copy to the new macro:
for( i=openBracket+1; i<closeBracket; ++i ) {
macro.body.push( tokens[i] );
}
macro.types = types.substr( openBracket+1, closeBracket-openBracket-1 );
// Remove from parsing:
splice( pos, closeBracket-pos+1, emptyStringVector, "" );
macros[macro.mangledName] = macro;
//macro.traceMacro();
}
private function expandTexture( pos:int ):int
{
var openParen:int = types.indexOf( "(", pos );
var closeParen:int = types.indexOf( ")", openParen+1 );
var openBracket:int = types.indexOf( "<", pos );
var closeBracket:int = types.indexOf( ">", openBracket+1 );
var eol:int = types.indexOf( "n", pos );
if ( eol < 0 ) eol = types.length;
//oc = tex.<2d,linear,miplinear>( v0, fs0 )
// to:
// tex oc, v0, fs0 <2d,linear,miplinear>"
var s:String = "tex " + tokens[pos] + "," + tokens[openParen+1] + "," + tokens[openParen+3] +
"<" + tokens.slice( openBracket+1, closeBracket ).join( "" ) + ">;";
var newTokens:Vector.<String> = tokenize( s );
var newTypes:String = tokenizeTypes( newTokens );
splice( pos, eol-pos, newTokens, newTypes );
return pos + newTypes.length;
}
private function expandMacro( pos:int ):void
{
// Macro is:
// ident = ident(
var NAME:int = 2;
var OPENPAREN:int = 3;
var closeParen:int = 0;
var i:uint = 0;
var name:String = tokens[pos+NAME];
closeParen = types.indexOf( ")", pos+OPENPAREN ) - pos;
var argc:int = ( closeParen - OPENPAREN ) / 2;
var mangledName:String = createMangledName( name, argc );
if ( macros[ mangledName ] == null )
throw new Error( "Macro '"+mangledName+"' not found." );
var macro:Macro = macros[mangledName];
var output:String = tokens[pos];
var args:Vector.<String> = tokens.slice( pos+OPENPAREN+1, pos+closeParen );
var body:Vector.<String> = new Vector.<String>();
var macroBodyLength:uint = macro.body.length;
for ( i=0; i<macroBodyLength; ++i ) {
var processed:Boolean = false;
if ( macro.types.charAt( i ) == "i" ) {
if ( macro.body[i].substr( 0, 3 ) == "out" ) {
body.push( output + macro.body[i].substr( 3 ) );
processed = true;
}
else {
var index:int = macro.args.indexOf( macro.body[i] );
if ( index >= 0 ) {
body.push( args[2*index] ); // parameter substitution
processed = true;
}
}
}
if ( !processed ) {
body.push( macro.body[i] );
}
}
splice( pos, // where to start
closeParen+1,
body, macro.types );
}
private function getConstant( numConst:String ):String
{
var num:Number = Number(numConst);
// Search the aliases, with constants, for something that works.
for each ( var agalVar:AGALVar in aliases ) {
if ( agalVar.isConstant() ) {
if ( agalVar.x == num ) return agalVar.target + ".x";
if ( agalVar.y == num ) return agalVar.target + ".y";
if ( agalVar.z == num ) return agalVar.target + ".z";
if ( agalVar.w == num ) return agalVar.target + ".w";
}
}
throw new Error( "Numeric constant used that is not declared in a constant register." );
return "error";
}
private function readAlias( pos:int ):void
{
// "alias va3.x, xform0Ref \n" +
// "alias va3.y, xform1Ref \n" +
// "alias va3.z, weight0 \n" +
if ( tokens[pos] == "alias" ) {
var agalVar:AGALVar = new AGALVar();
agalVar.name = tokens[pos+3];
agalVar.target = tokens[pos+1];
aliases[ agalVar.name ] = agalVar;
//trace( "alias name=" + agalVar.name + " target=" + agalVar.target );
if ( tokens[pos+4] == "(" ) {
// Read the default value.
var end:int = tokens.indexOf( ")", pos+5 );
if ( end < 0 ) throw new Error( "Closing paren of default alias value not found." );
agalVar.x = 0;
agalVar.y = 0;
agalVar.z = 0;
agalVar.w = 0;
if ( end > (pos+5) ) agalVar.x = Number( tokens[pos+5] );
if ( end > (pos+7) ) agalVar.y = Number( tokens[pos+7] );
if ( end > (pos+9) ) agalVar.z = Number( tokens[pos+9] );
if ( end > (pos+11) ) agalVar.w = Number( tokens[pos+11] );
}
}
}
// Return the new pos.
private function processTokens( pos:int, newline:int ):int
{
var brackets:int = 0;
if ( types.length >= 4 && types.substr( pos, 4 ) == "i=i(" ) {
// Macro!
expandMacro( pos );
return pos; // re-process. Could be anything.
}
else if ( types.length >= 4
&& types.substr( pos, 4 ) == "i=i<"
&& tokens[pos+2] == "tex" )
{
// Special texture handling.
return expandTexture( pos );
}
else if ( tokens[pos] == "alias" ) {
readAlias( pos );
splice( pos, newline-pos+1, null, null );
return pos;
}
else if ( tokens[pos] == "macro" ) {
processMacro( pos ); // macros remove themselves
return pos;
}
// Substitute aliases
// Substitute constants (a special kind of alias)
for ( var p:int = pos; p<newline; ++p ) {
var t:String = types.charAt(p);
if ( t == "[" ) {
++brackets;
}
else if ( t == "]" ) {
--brackets;
}
else if ( t == "0" ) {
if ( brackets == 0 ) {
tokens[p] = getConstant( tokens[p] );
types = types.substr( 0, p ) + "i" + types.substr( p+1 );
}
}
else if ( t == "i" ) {
// foo.xy is a valid token:
var dot:int = tokens[p].indexOf( "." );
if ( dot < 0 ) dot = tokens[p].length;
var agalVar:AGALVar = aliases[ tokens[p].substr(0,dot) ];
if ( agalVar != null ) {
tokens[p] = agalVar.target + tokens[p].substr( dot );
}
}
}
// finally, do math conversion.
if ( convertMath( pos ) ) {
// changes length; reset newline.
newline = types.indexOf( "n", pos+1 );
if ( newline < 0 ) newline = types.length;
}
return newline + 1;
}
private function mainLoop():void
{
var pos:int = 0;
var newline:int = 0;
var processing:Boolean = true;
while ( pos < tokens.length ) {
// Read in a line.
while( pos < tokens.length && types.charAt(pos) == "n" ) {
++pos;
}
if ( pos == tokens.length )
break;
newline = types.indexOf( "n", pos+1 );
if ( newline < 0 )
newline = types.length;
var type:String = types.charAt( pos );
if ( type == "#" ) {
if ( preproc == null ) preproc = new AGALPreAssembler();
processing = preproc.processLine( tokens.slice( pos, newline ), types.substr( pos, newline-pos ) );
// The preprocessing line is consumed.
splice( pos, newline-pos+1, null, null );
}
else {
if ( processing ) {
// Keep the tokens, update the position.
pos = processTokens( pos, newline );
}
else {
splice( pos, newline-pos+1, null, null );
}
}
}
}
/** Generate AGAL byte code from source. */
override public function assemble( mode:String, source:String ):ByteArray
{
if ( profile ) {
var start:uint = getTimer();
stages = new Vector.<PerfStage>();
stages.push( new PerfStage( "start" ));
}
isFrag = (mode=="fragment");
source = STDLIB + source;
// Note that Macros are object scope (kept around from run to run)
// but Alias are program scope
aliases = new Dictionary();
source = cleanInput( source ); // C-style comments /* */ makes this a little tricky per line.
tokens = tokenize( source );
types = tokenizeTypes( tokens );
mainLoop();
if ( profile ) {
stages.push( new PerfStage( "join" ));
}
asmCode = joinTokens( tokens );
if ( profile ) {
stages.push( new PerfStage( "mini" ));
}
var result:ByteArray = super.assemble( mode, asmCode );
if ( profile ) {
stages.push( new PerfStage( "end" ));
for( var k:int=0; k<stages.length-1; ++k ) {
var desc:String = stages[k].name + " --> " + stages[k+1].name + " = " + ((stages[k+1].time-stages[k].time)/1000);
trace( desc );
profileTrace += desc + "\n";
}
}
return result;
}
public static const STDLIB:String =
// Matrix multiply 3x3
"macro mul3x3( vec, mat ) {" +
" m33 out, vec, mat; " +
"}" +
// Matrix multiply 4x5
"macro mul4x4( vec, mat ) {" +
" m44 out, vec, mat; " +
"}"
}
}
internal class Macro
{
public var mangledName:String = "";
public var name:String = "";
public var args:Vector.<String> = new Vector.<String>();
public var body:Vector.<String> = new Vector.<String>();
public var types:String = "";
public function traceMacro():void
{
trace( "Macro: " + name + " [" + mangledName + "]" );
trace( " args: " + args );
trace( "<==" );
var s:String = com.adobe.utils.AGALMacroAssembler.joinTokens( body );
trace( s );
trace( "==>" );
}
}
internal class PerfStage
{
import flash.utils.*;
public var name:String;
public var time:uint;
public function PerfStage( name:String ) {
this.name = name;
this.time = getTimer();
}
}