#include "bytecode.h" // Implementation of the Slang bytecode (BC) // (most notably including conversion from IR to BC) #include "compiler.h" #include "ir.h" #include "ir-insts.h" #include "lower-to-ir.h" namespace Slang { struct SharedBytecodeGenerationContext; // Representation of a `BCPtr` during actual bytecode generation. // This representation is to deal with the fact that the actual // storage for the bytecode data might get reallocated during emission // so that we need to be careful and not work with raw `BCPtr`. template struct BytecodeGenerationPtr { SharedBytecodeGenerationContext* sharedContext; UInt offset; BytecodeGenerationPtr() : sharedContext(nullptr) , offset(0) {} BytecodeGenerationPtr( SharedBytecodeGenerationContext* sharedContext, UInt offset) : sharedContext(sharedContext) , offset(offset) {} BytecodeGenerationPtr( BytecodeGenerationPtr const& ptr) : sharedContext(ptr.sharedContext) , offset(ptr.offset) {} template BytecodeGenerationPtr( BytecodeGenerationPtr const& ptr, typename EnableIf::Value, void>::type * = 0) : sharedContext(ptr.sharedContext) , offset(ptr.offset) {} operator BCPtr() { return BCPtr(getPtr()); } T* operator->() { return getPtr(); } T& operator*() { return *getPtr(); } T& operator[](UInt index) { return getPtr()[index]; } BytecodeGenerationPtr operator+(Int index) { return BytecodeGenerationPtr( sharedContext, offset + index*sizeof(T)); } T* getPtr() const; }; #if 0 template void BCPtr::operator=(BytecodeGenerationPtr const& ptr) { fprintf(stderr, "0x%p: operator=BGP 0x%p\n", this, ptr.getPtr()); *this = ptr.getPtr(); } #endif struct SharedBytecodeGenerationContext { // The final generated bytecode stream List bytecode; // Map from a global symbol to its global ID Dictionary mapGlobalSymbolToGLobalID; }; struct BytecodeGenerationContext { SharedBytecodeGenerationContext* shared; // The function that is in scope for this context IRFunc* currentIRFunc; // Counter for global symbols that have been assigned // so that they can be used by this function List remappedGlobalSymbols; // Map an instruction to its ID for use local // to the current context Dictionary mapInstToLocalID; // Map an instruction to the ID for its auxiliary // symbol data Dictionary mapInstToNestedID; }; template T* BytecodeGenerationPtr::getPtr() const { if(!sharedContext) return nullptr; return (T*)(sharedContext->bytecode.Buffer() + offset); } BCPtr::RawVal allocateRaw( BytecodeGenerationContext* context, size_t size, size_t alignment) { size_t currentOffset = context->shared->bytecode.Count(); size_t beginOffset = (currentOffset + (alignment-1)) & ~(alignment-1); size_t endOffset = beginOffset + size; for(size_t ii = currentOffset; ii < endOffset; ++ii) context->shared->bytecode.Add(0); return beginOffset; } template BytecodeGenerationPtr allocate( BytecodeGenerationContext* context) { return BytecodeGenerationPtr(context->shared, allocateRaw(context, sizeof(T), alignof(T))); } template BytecodeGenerationPtr allocateArray( BytecodeGenerationContext* context, UInt count) { return BytecodeGenerationPtr(context->shared, allocateRaw(context, count * sizeof(T), alignof(T))); } template BytecodeGenerationPtr getPtr( BytecodeGenerationContext* context) { return BytecodeGenerationPtr(context->shared, context->shared->bytecode.Count()); } void encodeUInt8( BytecodeGenerationContext* context, uint8_t value) { context->shared->bytecode.Add(value); } void encodeUInt( BytecodeGenerationContext* context, UInt value) { if( value < 128 ) { encodeUInt8(context, value); return; } uint8_t bytes[16]; UInt count = 0; for(;;) { UInt index = count++; bytes[index] = value & 0x7F; value = value >> 7; if (!value) break; bytes[index] |= 0x80; } UInt index = count; while (index--) { encodeUInt8(context, bytes[index]); } } void encodeSInt( BytecodeGenerationContext* context, Int value) { UInt uValue; if( value < 0 ) { uValue = (~UInt(value) << 1) | 1; } else { uValue = UInt(value) << 1; } encodeUInt(context, uValue); } Int getLocalID( BytecodeGenerationContext* context, IRInst* inst) { Int localID = 0; if( context->mapInstToLocalID.TryGetValue(inst, localID) ) { return localID; } Int globalID = 0; if( context->shared->mapGlobalSymbolToGLobalID.TryGetValue(inst, globalID) ) { BCConst bcConst; bcConst.globalID = globalID; UInt remappedSymbolIndex = context->remappedGlobalSymbols.Count(); context->remappedGlobalSymbols.Add(bcConst); localID = ~remappedSymbolIndex; context->mapInstToLocalID.Add(inst, localID); return localID; } SLANG_UNEXPECTED("no ID for inst"); return -9999; } void encodeOperand( BytecodeGenerationContext* context, IRInst* operand) { auto id = getLocalID(context, operand); encodeSInt(context, id); } bool opHasResult(IRInst* inst) { auto type = inst->getType(); if( !type || type->op != kIROp_VoidType ) { return true; } return false; } void generateBytecodeForInst( BytecodeGenerationContext* context, IRInst* inst) { // We are generating bytecode for a local instruction // inside a function or similar context. switch( inst->op ) { default: { // As a default case, we will assume that bytecode ops // and the IR's internal opcodes are the same, and then // encode the necessary extra info: // auto argCount = inst->getArgCount(); encodeUInt(context, inst->op); encodeUInt(context, argCount); for( UInt aa = 0; aa < argCount; ++aa ) { encodeOperand(context, inst->getArg(aa)); } auto type = inst->getType(); if( type && type->op == kIROp_VoidType ) { // This instructions has no type, so don't emit a destination } else { // The instruction can be encoded // as its own operand for the destination. encodeOperand(context, inst); } } break; case kIROp_ReturnVoid: // Trivial encoding here encodeUInt(context, inst->op); break; case kIROp_IntLit: { auto ii = (IRConstant*) inst; encodeUInt(context, ii->op); encodeOperand(context, ii->getType()); // TODO: probably want distinct encodings // for signed vs. unsigned here. encodeUInt(context, UInt(ii->u.intVal)); // destination: encodeOperand(context, inst); } break; case kIROp_FloatLit: { auto ii = (IRConstant*) inst; encodeUInt(context, ii->op); encodeOperand(context, ii->getType()); static const UInt size = sizeof(IRFloatingPointValue); unsigned char buffer[size]; memcpy(buffer, &ii->u.floatVal, sizeof(buffer)); for(UInt ii = 0; ii < size; ++ii) { encodeUInt8(context, buffer[ii]); } // destination: encodeOperand(context, inst); } break; case kIROp_boolConst: { auto ii = (IRConstant*) inst; encodeUInt(context, ii->op); encodeUInt(context, ii->u.intVal ? 1 : 0); // destination: encodeOperand(context, inst); } break; case kIROp_Func: { encodeUInt(context, inst->op); // We just want to encode the ID for the function // symbol data, and then do the rest on the decode side UInt nestedID = 0; context->mapInstToNestedID.TryGetValue(inst, nestedID); encodeUInt(context, nestedID); // destination: encodeOperand(context, inst); } break; case kIROp_Store: { encodeUInt(context, inst->op); // We need to encode the type being stored, to make // our lives easier. encodeOperand(context, inst->getArg(2)->getType()); encodeOperand(context, inst->getArg(1)); encodeOperand(context, inst->getArg(2)); } break; case kIROp_Load: { encodeUInt(context, inst->op); encodeOperand(context, inst->getType()); encodeOperand(context, inst->getArg(1)); encodeOperand(context, inst); } break; } } Int getIDForGlobalSymbol( BytecodeGenerationContext* context, IRInst* inst) { Int globalID; if(context->shared->mapGlobalSymbolToGLobalID.TryGetValue(inst, globalID)) return globalID; SLANG_UNEXPECTED("no such ID"); } uint32_t getTypeForGlobalSymbol( BytecodeGenerationContext* context, IRInst* inst) { auto type = inst->getType(); if(!type) return 0; return getIDForGlobalSymbol(context, type); } BytecodeGenerationPtr allocateString( BytecodeGenerationContext* context, char const* data, UInt size) { BytecodeGenerationPtr ptr = allocateArray(context, size + 1); memcpy(ptr.getPtr(), data, size); return ptr; } BytecodeGenerationPtr allocateString( BytecodeGenerationContext* context, String const& str) { return allocateString(context, str.Buffer(), str.Length()); } BytecodeGenerationPtr allocateString( BytecodeGenerationContext* context, Name* name) { return allocateString(context, name->text); } BytecodeGenerationPtr tryGenerateNameForSymbol( BytecodeGenerationContext* context, IRInst* inst) { // TODO: this is gross, and the IR should probably have // a more direct means of querying a name for a symbol. if (auto highLevelDeclDecoration = inst->findDecoration()) { auto decl = highLevelDeclDecoration->decl; if (auto reflectionNameMod = decl->FindModifier()) { return allocateString(context, reflectionNameMod->name); } else if(auto name = decl->nameAndLoc.name) { return allocateString(context, name); } } return BytecodeGenerationPtr(); } BytecodeGenerationPtr generateBytecodeSymbolForInst( BytecodeGenerationContext* context, IRInst* inst) { switch( inst->op ) { case kIROp_Func: { auto irFunc = (IRFunc*) inst; BytecodeGenerationPtr bcFunc = allocate(context); bcFunc->op = inst->op; bcFunc->typeGlobalID = getTypeForGlobalSymbol(context, inst); BytecodeGenerationContext subContextStorage; BytecodeGenerationContext* subContext = &subContextStorage; subContext->shared = context->shared; subContext->currentIRFunc = irFunc; // First we need to enumerate our basic blocks, so that they // can reference one another (basic blocks can forward reference // blocks that haven't been seen yet). // // Note: we allow the IDs of blocks to overlap with ordinary // "register" numbers, because there is no case where an operand // could be either a block or an ordinary register. // UInt blockCounter = 0; for( auto bb = irFunc->getFirstBlock(); bb; bb = bb->getNextBlock() ) { Int blockID = Int(blockCounter++); subContext->mapInstToLocalID.Add(bb, blockID); } UInt blockCount = blockCounter; // Allocate the array of block objects to be stored in the // bytecode file. auto bcBlocks = allocateArray(context, blockCount); bcFunc->blockCount = blockCount; bcFunc->blocks = bcBlocks; // Now loop through the blocks again, and allocate the storage // for any parameters, variables, or registers used inside // each block. // // We'll count in a first pass, and then fill things in // using a second pass Int regCounter = 0; blockCounter = 0; for( auto bb = irFunc->getFirstBlock(); bb; bb = bb->getNextBlock() ) { UInt blockID = blockCounter++; UInt paramCount = 0; for( auto ii = bb->firstChild; ii; ii = ii->nextInst ) { switch( ii->op ) { default: // Default behavior: if an op has a result, // then it needs a register to store it. if(opHasResult(ii)) { regCounter++; } break; case kIROp_Param: // A parameter always uses a register. regCounter++; // // We also want to keep a count of the parameters themselves. paramCount++; break; case kIROp_Var: // A `var` (`alloca`) node needs two registers: // one to hold the actual storage, and another // to hold the pointer. regCounter += 2; break; } } bcBlocks[blockID].paramCount = paramCount; } // Okay, we've counted how many registers we need for each block, // and now we can allocate the contiguous array we will use. UInt regCount = regCounter; auto bcRegs = allocateArray(context, regCount); bcFunc->regCount = regCount; bcFunc->regs = bcRegs; // Now we will loop over things again to fill in the information // on all these registers. regCounter = 0; blockCounter = 0; for( auto bb = irFunc->getFirstBlock(); bb; bb = bb->getNextBlock() ) { UInt blockID = blockCounter++; // Loop over just the parameters first, to ensure they // are always the first N registers of a block. // // This means the parameters of the function itself // are always the first N registers in the overall list. // bcBlocks[blockID].params = bcRegs + regCounter; for( auto ii = bb->firstChild; ii; ii = ii->nextInst ) { if(ii->op != kIROp_Param) continue; Int localID = regCounter++; subContext->mapInstToLocalID.Add(ii, localID); bcRegs[localID].op = ii->op; bcRegs[localID].name = tryGenerateNameForSymbol(context, ii); bcRegs[localID].previousVarIndexPlusOne = localID; bcRegs[localID].typeGlobalID = getTypeForGlobalSymbol(context, ii); } // Now loop over the non-parameter instructions and // allocate actual register locations to them. for( auto ii = bb->firstChild; ii; ii = ii->nextInst ) { switch(ii->op) { case kIROp_Param: // Already handled. break; default: // For an ordinary instruction with a result, // allocate it here. if( opHasResult(ii) ) { Int localID = regCounter++; subContext->mapInstToLocalID.Add(ii, localID); bcRegs[localID].op = ii->op; bcRegs[localID].name = tryGenerateNameForSymbol(context, ii); bcRegs[localID].previousVarIndexPlusOne = localID; bcRegs[localID].typeGlobalID = getTypeForGlobalSymbol(context, ii); } break; case kIROp_Var: { // As handled in the earlier loop, we are // allocating *two* locations for each `var` // instruction. The first of these will be // the actual pointer value, while the second // will be the storage for the variable value. Int localID = regCounter; regCounter += 2; subContext->mapInstToLocalID.Add(ii, localID); bcRegs[localID].op = ii->op; bcRegs[localID].name = tryGenerateNameForSymbol(context, ii); bcRegs[localID].previousVarIndexPlusOne = localID; bcRegs[localID].typeGlobalID = getTypeForGlobalSymbol(context, ii); bcRegs[localID+1].op = ii->op; bcRegs[localID+1].previousVarIndexPlusOne = localID+1; bcRegs[localID+1].typeGlobalID = getIDForGlobalSymbol(context, ((IRPtrType*) ii->getType())->getValueType()); } break; } } } // Now that we've allocated our blocks and our registers // we can go through the actual process of emitting instructions. Hooray! blockCounter = 0; for( auto bb = irFunc->getFirstBlock(); bb; bb = bb->getNextBlock() ) { UInt blockID = blockCounter++; bcBlocks[blockID].code = getPtr(context); for( auto ii = bb->firstChild; ii; ii = ii->nextInst ) { // What we do with each instruction depends a bit on the // kind of instruction it is. switch( ii->op ) { default: // For most instructions we just emit their bytecode // ops directly. generateBytecodeForInst(subContext, ii); break; case kIROp_Param: // Don't actually emit code for these, because // there isn't really anything to *execute* // // Note that we *do* allow the `var` nodes // to be executed, just because they need // to set up a register with the pointer value. break; } } } // Finally, after emitting all the instructions we can // build a table of global symbols taht need to be // imported into the current function as constants. UInt constCount = subContext->remappedGlobalSymbols.Count(); auto bcConsts = allocateArray(context, constCount); bcFunc->constCount = constCount; bcFunc->consts = bcConsts; for( UInt cc = 0; cc < constCount; ++cc ) { bcConsts[cc] = subContext->remappedGlobalSymbols[cc]; } return bcFunc; } break; default: // Most instructions don't need a custom representation. return BytecodeGenerationPtr(); } } BytecodeGenerationPtr generateBytecodeForModule( BytecodeGenerationContext* context, IRModule* irModule) { // The module will get encoded much like a function, // and then that function will be "invoked" to load // the module. // auto bcModule = allocate(context); bcModule->op = irModule->op; bcModule->typeGlobalID = 0; // The logical function that the module representats // will only have a single block, so we can allocate it now. // auto bcBlock = allocate(context); bcBlock->paramCount = 0; bcBlock->params = BytecodeGenerationPtr(); bcModule->blockCount = 1; bcModule->blocks = bcBlock; // Because the module is the top-most level, there is // no need for it to have "constants" that represent // values imported from the next outer scope. // bcModule->constCount = 0; bcModule->consts = BytecodeGenerationPtr(); // We need to compute how many "registers" to allocate // for the module, where the registers represent the // values being computed at the global scope. UInt regCount = 0; for( auto inst = irModule->firstChild; inst; inst = inst->nextInst ) { if(!opHasResult(inst)) continue; Int globalID = Int(regCount++); context->shared->mapGlobalSymbolToGLobalID.Add(inst, globalID); // In the global scope, global IDs are also the local IDs context->mapInstToLocalID.Add(inst, globalID); } auto bcRegs = allocateArray(context, regCount); bcModule->regCount = regCount; bcModule->regs = bcRegs; // Now lets walk through and initialize all those bytecode // register representations so that we can use them. UInt regCounter= 0; for( auto inst = irModule->firstChild; inst; inst = inst->nextInst ) { if(!opHasResult(inst)) continue; UInt regIndex = *context->mapInstToLocalID.TryGetValue(inst); BytecodeGenerationPtr name = tryGenerateNameForSymbol(context, inst); bcRegs[regIndex].op = inst->op; bcRegs[regIndex].name = name; bcRegs[regIndex].typeGlobalID = getTypeForGlobalSymbol(context, inst); bcRegs[regIndex].previousVarIndexPlusOne = regIndex; } // Some instructions represent "nested" symbols that will need // custom handling, and we will represent those here. List> nestedSymbols; for( auto inst = irModule->firstChild; inst; inst = inst->nextInst ) { UInt regIndex = *context->mapInstToLocalID.TryGetValue(inst); auto bcSymbol = generateBytecodeSymbolForInst(context, inst); if (!bcSymbol.getPtr()) continue; UInt nestedSymbolID = nestedSymbols.Count(); nestedSymbols.Add(bcSymbol); context->mapInstToNestedID.Add(inst, nestedSymbolID); bcSymbol->name = bcRegs[regIndex].name; } auto nestedSymbolCount = nestedSymbols.Count(); auto bcNestedSymbols = allocateArray>(context, nestedSymbolCount); bcModule->nestedSymbolCount = nestedSymbolCount; bcModule->nestedSymbols = bcNestedSymbols; for (UInt ii = 0; ii < nestedSymbolCount; ++ii) { bcNestedSymbols[ii] = nestedSymbols[ii]; } // Finally, we can go through and emit the actual code for // the initialization step. bcBlock->code = getPtr(context); for( auto inst = irModule->firstChild; inst; inst = inst->nextInst ) { // Generate bytecode for global-scope inst generateBytecodeForInst(context, inst); } // Need to encode a terminator here, just to keep the encoding valid encodeUInt(context, kIROp_ReturnVoid); #if 0 // Now we can go through and generate the bytecode object // that will represent each of these global symbols List> globalSymbols; for( auto inst = irModule->firstChild; inst; inst = inst->nextInst ) { // Generate bytecode for global-scope inst auto globalSymbol = generateBytecodeForGlobalSymbol(context, inst); globalSymbols.Add(globalSymbol); } #endif return bcModule; } void generateBytecodeStream( BytecodeGenerationContext* context, IRModule* irModule) { // Header must be the very first thing in the bytecode stream BytecodeGenerationPtr header = allocate(context); memcpy(header->magic, "slang\0bc", sizeof(header->magic)); header->version = 0; // HACK: ensure that a NULL pointer in an operand field can // be encoded. context->shared->mapGlobalSymbolToGLobalID.Add(nullptr, -1); header->module = generateBytecodeForModule(context, irModule); } List emitSlangIRForEntryPoint( EntryPointRequest* entryPoint) { auto compileRequest = entryPoint->compileRequest; auto irModule = lowerEntryPointToIR( entryPoint, compileRequest->layout.Ptr(), // TODO: we need to pick the target more carefully here CodeGenTarget::HLSL); #if 0 String irAsm = getSlangIRAssembly(irModule); fprintf(stderr, "%s\n", irAsm.Buffer()); #endif // Now we need to encode that IR into a binary format // for transmission/serialization/etc. SharedBytecodeGenerationContext sharedContext; BytecodeGenerationContext context; context.shared = &sharedContext; generateBytecodeStream(&context, irModule); return sharedContext.bytecode; } } // namespace Slang