Chris Shader IR¶
Sobre este capítulo Sombreamento programável
Neste capítulo
- Chris Shader IR
- Escopo
- Capacidades fixas
- Formato da instruction
- Não é SSA
- Modelo de temporaries
- Integers e booleans
- Pool de immediates
- Constant lowering
- Movimento de dados
- Arithmetic
- Divisão por reciprocal
- Built-ins como composição de IR
- Matrices
- Texture sampling
- Loads de interface
- Stores de interface
- Remap de varying
- Control flow linear
- Semântica de IF
- ELSE e ENDIF
- Discard
- Emissão de instructions
- Prologue semântico
- Desaparecimento dos nomes locais
- Otimização
- Limitações do optimizer
- Particularidade de SETLANE
- Verificação
- Validação de temporary
- Regras por stage
- Validação de slots
- Constants
- Matrix verification
- Dump textual
- Inspeção pública
- Execução software
- Execução versus verification
- Consumo pelo TGSI
- Formato CSI
- Portabilidade do CSI
- Checksum CSI
- Loading de CSI
- Lacuna de metadata do CSI
- Consequência no verifier
- Evidência de testes
- Evidência cross-backend
- Complexidade
- Fronteira de robustez
- Limitações atuais
- Testes futuros recomendados
- Nota de revisão
Escopo¶
Chris Shader IR, ou CSIR, é a representação intermediária compacta compartilhada pelos dois caminhos de execução do compiler de shaders do ChrisOS.
Depois que o frontend semelhante a GLSL resolve scopes, types, interface slots, constructors, built-ins e control flow limitado, o lowering semântico produz CSIR.
A mesma sequência de instructions é consumida por:
- emitter textual TGSI usado por VirGL;
- interpreter software usado na execução CPU.
CSIR é, portanto, a fronteira semântica central do subsystem de shaders.
A IR é pequena, linear e limitada. Não é SSA e não é uma IR geral de compilador.
Capacidades fixas¶
O compiler mantém CSIR dentro de ShComp.
Os limites principais são:
Não existe crescimento dinâmico acima desses valores.
Se o lowering precisar exceder um pool, a compilação produz erro e deixa de gerar shader válido.
Isso mantém memória e custo previsíveis no kernel.
Formato da instruction¶
Cada instruction é:
typedef struct Ir {
uint8_t op;
uint8_t ty;
uint8_t ncomp;
uint8_t pad;
int16_t dst;
int16_t a, b, c;
int16_t aux;
int16_t aux2;
} Ir;
Com esse layout, cada record ocupa 16 bytes.
O array máximo de 384 entries ocupa 6144 bytes dentro de ShComp.
Os campos aux e aux2 têm significado diferente conforme o opcode.
Não é SSA¶
Temporaries são índices inteiros de um register file fixo.
Não existem versions SSA, phi nodes ou ownership por basic block.
Uma instruction pode escrever em temporary já existente.
IR_SETLANE, por exemplo, modifica somente uma lane de um destination vector existente.
Por isso o optimizer precisa ser conservador: pressupostos comuns de SSA não se aplicam.
Modelo de temporaries¶
O interpreter materializa:
Cada temporary comum se comporta como register float de quatro lanes.
Scalars usam lane zero.
Vectors usam uma a quatro lanes.
Matrices usam temporaries consecutivos, uma column por temporary.
Mat3 consome três temporaries; mat4 consome quatro.
O type field mantém semântica de tipo, mas o storage físico do interpreter é baseado em float.
Integers e booleans¶
Integers e booleans também usam o mesmo storage float no backend software.
IR_TRUNC faz conversão para integer sem criar outro register file.
Comparisons produzem 1.0 para true e 0.0 para false.
Control flow consulta lane zero contra zero.
A IR é tipada semanticamente, mas não possui register files físicos separados para int e float.
Pool de immediates¶
Literals ficam em:
IR_CONST usa aux como índice inicial e ncomp como quantidade de values.
A execução copia os immediates para o destination e zera lanes não usadas.
Separar os constants mantém o record de instruction com tamanho fixo.
Constant lowering¶
A semantic analysis já faz parte do constant folding antes de emitir IR.
Arithmetic com values conhecidos pode produzir novo temporary backed por immediate em vez de arithmetic runtime.
Loop bounds são avaliados estaticamente.
Constructors também podem materializar vectors e matrix columns constantes.
Assim, CSIR recebe programa parcialmente simplificado, não tradução literal da AST.
Movimento de dados¶
A família básica contém:
IR_MOV copia o número indicado de components.
IR_SWZ usa dois bits por lane em aux para escolher components.
IR_SETLANE grava uma lane do destination a partir de uma lane do source; aux seleciona destino e aux2 seleciona origem.
Essas operações sustentam constructors, assignments e montagem de vectors.
Arithmetic¶
Os principais opcodes são:
Operações component-wise usam ncomp.
IR_DOT usa aux como largura.
IR_CMP usa:
O resultado software é boolean scalar representado como 0.0 ou 1.0.
Divisão por reciprocal¶
Não existe opcode separado de divide.
Division é lowered para reciprocal seguido de multiply.
Isso reduz o conjunto da IR e combina bem com backends que já oferecem reciprocal.
No interpreter software, reciprocal de zero produz zero.
Divisão por scalar constant zero é rejeitada antes, durante semantic analysis.
Casos excepcionais de floating point devem ser testados explicitamente entre backends.
Built-ins como composição de IR¶
Vários built-ins do source não possuem opcode próprio.
Exemplos:
normalize: dot + reciprocal square root + multiply;length: dot + RSQ + reciprocal;clamp: max + min;mix: arithmetic;reflect: dot + vector arithmetic;cross: swizzles + multiplies + subtracts.
O instruction set CSIR é menor que o conjunto de built-ins da linguagem.
Matrices¶
Existem dois opcodes dedicados:
Matrices seguem semântica column-major no shader subsystem.
Em matrix-vector multiply, aux carrega a quantidade de columns, normalmente três ou quatro.
Matrix-matrix grava temporaries consecutivos, uma column de output por temporary.
O verifier aceita largura 3 ou 4 para IR_MULMM.
Texture sampling¶
IR_SAMPLE representa sample 2D.
aux é o sampler slot.
O coordinate vem do temporary a.
O destination recebe quatro components.
A IR não codifica texture object, filtering ou addressing completos; isso pertence ao runtime/backend.
O interpreter software recebe um único CPU texture pointer e aplica seu sampling nearest/clamp.
O TGSI backend usa o sampler slot para declarations e instruction correspondente.
Loads de interface¶
Os loads são:
IR_LOAD_ATTR usa aux como attribute location.
IR_LOAD_VAR usa aux como varying slot.
IR_LOAD_UNI usa aux como base vec4 constant slot e aux2 como quantidade de vec4 slots consecutivos.
Isso permite carregar mat3 e mat4 em temporaries consecutivos.
IR_LOAD_FCOORD carrega fragment coordinate fornecida pelo backend.
Stores de interface¶
Outputs usam:
IR_STORE_POS só é válido em vertex shader.
IR_STORE_VAR grava varying indicado por aux.
IR_STORE_COLOR só é válido em fragment shader.
Como a linguagem atual possui apenas um color output, não existe render-target index nessa instruction.
Remap de varying¶
Fragment shader compilado inicialmente usa seus próprios varying slots.
No link, inputs do fragment são associados aos outputs do vertex por name e type.
TGSI emitter e software executor podem receber var_remap.
Para IR_LOAD_VAR, o slot remapeado substitui o slot local do fragment shader.
Isso permite compilar cada stage isoladamente e resolver a interface final somente no link.
Control flow linear¶
CSIR possui apenas:
Não existem branch targets, labels, jumps, runtime loops, phi nodes ou function calls.
O for da linguagem é unrolled antes.
User functions são semanticamente inlined.
A IR final é uma lista linear com conditionals estruturados aninhados.
Semântica de IF¶
IR_IF lê lane zero do temporary a.
Zero é false; qualquer valor diferente é true.
O interpreter mantém stacks fixas de 32 entries para skip e branch state.
Branches aninhados dentro de branch já skipped permanecem skipped até o ENDIF correto.
O verifier checa balanceamento antes da execução.
ELSE e ENDIF¶
O verifier rejeita:
- ELSE sem IF;
- segundo ELSE no mesmo IF;
- ENDIF sem IF;
- nesting acima de 32;
- control-flow depth não zero no fim.
O interpreter também possui checks defensivos.
Assim, malformed flow é rejeitado estruturalmente e também possui proteção runtime.
Discard¶
IR_DISCARD é válido somente em fragment shader.
No interpreter ele marca discarded quando o pointer existe e termina imediatamente a execução do shader.
No backend TGSI vira a operação de kill/discard correspondente.
Não existe discard no vertex stage.
Emissão de instructions¶
O lowering usa um helper emit.
Quando nir chega a 384, o compiler registra:
Temporary allocation também falha acima de 96.
Immediate allocation falha acima de 160 floats.
Os limites são impostos já durante construção, antes do verifier.
Prologue semântico¶
Antes de lowering de main, o compiler emite um prologue com base nas interfaces registradas.
Ele aloca temporaries e produz:
- uniform loads;
- vertex attribute loads;
- fragment varying loads;
- fragment-coordinate load;
- inicialização zero dos stage outputs e de
gl_Position.
O body do main trabalha sobre esses temporaries.
Por isso a IR já contém interface traffic explícito em vez de nomes de variables.
Desaparecimento dos nomes locais¶
A maior parte da identidade dos symbols não aparece na instruction stream.
Uniforms, attributes, varyings e samplers viram numeric slots.
Locals viram temporaries.
Functions são inlined.
A symbol table ainda existe em ShComp para dumps, linking e metadata, mas o stream não depende dos names locais.
Otimização¶
sh_opt executa quatro passes.
Cada pass primeiro conta uses dos temporaries.
Depois, para um conjunto de instructions puras, se o destination possui zero uses, o opcode vira IR_NOP.
Entram nessa classe constants, moves, swizzles, arithmetic básica, min/max, abs, dot, reciprocal, trig, pow, trunc, compare e matrix-vector multiply.
O conjunto é deliberadamente conservador.
Limitações do optimizer¶
Não é dead-code elimination completo.
Não remove branches unreachable inteiros.
Não compacta o array depois de converter instructions para NOP.
Não há constant-propagation dataflow geral, CSE, register allocation, SSA conversion ou análise de live intervals.
Os quatro passes permitem que remover um resultado revele producer anterior também sem uso.
Particularidade de SETLANE¶
IR_SETLANE modifica somente parte do destination.
O optimizer trata destination como use além de write durante a contagem.
Sem essa regra, values anteriores em lanes não alteradas poderiam ser considerados mortos de forma incorreta.
É uma consequência direta do modelo não-SSA.
Verificação¶
Depois da otimização, sh_verify valida a IR antes do TGSI ou da aceitação final.
Primeiro verifica stage, temporary count e instruction count.
Depois percorre cada non-NOP instruction.
A verificação é estrutural e baseada em ranges; não é prova formal da semântica completa do programa.
Validação de temporary¶
Temporary válido precisa obedecer:
Destinations e operands comuns são validados.
Operações multi-temporary, como uniform loads e matrix multiply, também validam a última posição consecutiva.
Isso impede acesso fora do register file alocado.
Regras por stage¶
O verifier exige:
IR_STORE_POSsomente em vertex;IR_STORE_COLORsomente em fragment;IR_DISCARDsomente em fragment.
Essa defesa também é aplicada a blobs CSI carregados, não apenas a source compilado.
Validação de slots¶
IR_SAMPLE precisa apontar sampler entre zero e samp_hi.
IR_LOAD_ATTR precisa ficar até attr_hi.
Varying load/store precisa estar em 0..7.
IR_LOAD_UNI valida destination span e quantidade de vectors.
O caminho normal do compiler controla o limite de 32 slots durante semantic allocation.
Constants¶
Para IR_CONST, o verifier exige:
- destination válido;
aux >= 0;aux + ncomp <= nimm.
Isso protege o pool de immediate.
O conteúdo floating-point não é autenticado.
NaN ou outros values são dados, não structural corruption.
Matrix verification¶
IR_MULMM aceita aux igual a 3 ou 4.
Destination span e source temporaries iniciais precisam ser válidos.
O normal lowering já aloca columns contíguas.
O verifier protege shape/range, mas não reconstrói toda a proveniência de types da matrix.
Dump textual¶
sh_dump_ir_buf gera representação legível.
O output começa com shader name e stage e lista interfaces como uniforms, inputs e outputs.
Depois aparece:
e cada instruction não-NOP com destination, mnemonic, operands selecionados e alguns campos aux.
O nome block 0 não significa que exista CFG geral com vários basic blocks; a IR continua linear.
Inspeção pública¶
sh_shader_ir expõe o dump armazenado no shader compilado.
Os testes usam essa interface para verificar que stores e outras operações apareceram.
É formato de debug, não interchange format estável.
A persistência binária usa CSI.
Execução software¶
sh_exec_ir executa CSIR diretamente.
O register file é zerado e outputs recebem defaults.
Depois cada instruction é percorrida em ordem.
Control markers atualizam a skip stack.
Instructions comuns dentro de branch skipped são ignoradas.
Arithmetic e interface operations manipulam o temporary array.
O interpreter serve como backend CPU e referência executável da semântica da IR.
Execução versus verification¶
O interpreter contém checks defensivos, mas espera normalmente CSIR previamente verificado.
Alguns handlers checam indices diretamente; outros dependem mais dos invariants estabelecidos por sh_verify.
O contrato arquitetural é:
Blob binário não confiável não deve pular o verifier.
Consumo pelo TGSI¶
O TGSI emitter percorre a mesma lista linear.
Ele converte CSIR em declarations e textual TGSI instructions.
Alguns opcodes CSIR se expandem em várias instructions TGSI.
Por isso instruction count entre as duas representações não precisa coincidir.
O capítulo seguinte trata desse mapping em detalhe.
Formato CSI¶
sh_shader_save_csi serializa CSIR como:
O header contém oito words de 32 bits:
O magic forma os bytes "CSIR" em little-endian.
Portabilidade do CSI¶
O corpo das instructions é cópia direta dos bytes do array Ir.
Diferente dos encoders VirtIO, os fields não são serializados um por um para um wire format canônico.
Logo, o formato pressupõe mesmo layout de Ir, endianness e ABI compatível entre writer e reader.
Deve ser considerado formato interno experimental, não padrão portátil cross-architecture.
Checksum CSI¶
O checksum é soma simples dos bytes dos records de IR.
O immediate pool não entra.
Modificar immediate pode deixar checksum igual.
O loader ainda valida tamanho e estrutura, mas não autentica os literal values.
Uma versão futura deveria proteger o payload completo e versionar explicitamente seu wire layout.
Loading de CSI¶
O loader valida:
- header mínimo;
- magic/version;
- stage vertex ou fragment;
- contagens dentro dos limites;
- tamanho total exatamente igual ao esperado;
- checksum;
sh_verify.
Depois tenta emitir TGSI.
Se o TGSI falhar, pode retornar shader object com ok = 0.
Lacuna de metadata do CSI¶
CSI salva apenas stage, IR, immediates e temporary count.
Não salva symbol table, uniform names/high-water metadata, attribute metadata, sampler metadata, varying declarations ou source.
Ao carregar, o novo ShComp é zerado e essa metadata não é reconstruída.
Isso torna roundtrip geral incompleto.
Consequência no verifier¶
A lacuna é observável.
Um shader carregado que use attribute slot acima de zero pode falhar porque attr_hi não foi restaurado.
Sampler slot acima de zero pode falhar porque samp_hi também não foi reconstruído.
O teste atual usa vertex shader constante sem interfaces complexas e, portanto, não cobre esse caso.
CSI ainda não deve ser tratado como production shader cache geral.
Evidência de testes¶
tools/test_shader.c verifica que shaders compilados expõem IR e que o mesmo CSIR executa pelo software backend.
A suite cobre matrices, texture sampling, lighting, conditionals, unrolled loops, functions, discard e trig.
Também salva shader simples, recarrega, altera um byte da região IR e exige rejeição da corrupção.
Ela não cobre roundtrip de metadata CSI complexa.
Evidência cross-backend¶
A mesma IR semântica alimenta software e TGSI.
tools/test_gfx3d_abi.c compara expectativas CPU com execução de vertex shader para identity, translation, rotations, scale, view e projection.
Isso ajuda a validar convenções de matrix antes da geração backend-specific.
Não prova bit-equivalence de todas as funções transcendentes entre CPU e VirGL.
Complexidade¶
Verification e dump são lineares no número de instructions.
Optimizer faz quatro scans de use counting e quatro de elimination, permanecendo O(IR) com multiplicador fixo pequeno.
Software execution é O(IR) por shader invocation, além de custo de sampling e frequência de pixels/vertices.
Com máximo de 384 instructions, o foco é previsibilidade.
Fronteira de robustez¶
Durante compile normal CSIR é memória do kernel controlada pelo compiler.
CSI introduz uma boundary de binary input.
Limites fixos, length checks e structural verification reduzem riscos de blobs malformados.
Porém, gaps de portabilidade, checksum e metadata significam que CSI não deve ser visto como interchange format endurecido para input não confiável sem trabalho adicional.
Limitações atuais¶
CSIR é linear, não-SSA e single-function depois do lowering.
Não possui runtime loops, calls, labels, jumps ou phi nodes.
O register file físico software é float-based mesmo para int/bool.
Optimization é mínima.
A serialização depende do ABI e não preserva interfaces completas.
A IR foi desenhada para o subset atual, não como IR universal.
Testes futuros recomendados¶
São úteis unit tests diretos do verifier para opcode inválido, flow desbalanceado, sampler/attribute fora da faixa, matrix spans inválidos e immediate overflow.
CSI deve ser testado com attribute location 3, vários uniforms, sampler slot 1 e varyings, exigindo reconstrução correta ou rejeição explícita.
Depois de fortalecer o formato, mutation de bytes do immediate pool também deve ser detectada.
Nota de revisão¶
Este capítulo foi criado contra a revisão ChrisOS e05a17fd76333114a3fb5c2452f38ca747d4ac56. Ele documenta CSIR como interface semântica limitada compartilhada pelo interpreter software e pelo backend TGSI, incluindo optimizer, verifier e limitações da persistência CSI.