Projeto pessoal / Arquitetura de engine

Tessera Engine

Uma game engine construída com responsabilidades explícitas e decisões medidas.

Tessera é a game engine e o editor para Windows que estou desenvolvendo em C++20, com Direct3D 12 e Agility SDK. Escrevo o ECS, o job system, o pipeline de assets e o renderizador. Este estudo explica como essas peças se conectam, por que defini suas fronteiras e como verifico seu funcionamento.

As decisões são medidas, não supostas. Testes por subsistema, sondas de linha de comando, baselines de benchmark e comparações de imagem acompanham a implementação. O repositório é privado; a arquitetura, as capturas e as medições documentadas aqui apresentam uma visão técnica do trabalho em andamento.

  • C++20
  • Windows
  • Direct3D 12
  • Dear ImGui
  • Sharpmake
Visão geral do editor com docking, viewport, hierarquia, inspector e Content Browser; esta captura mostra uma cena sem malhas cozidas carregadas.
Visão geral do editor com docking, viewport, hierarquia, inspector e Content Browser; esta captura mostra uma cena sem malhas cozidas carregadas. Abrir original em nova aba

Visão geral

Peças pequenas, fronteiras verificáveis

Tessera é o nome de uma peça de mosaico. Cada camada tem uma responsabilidade e uma direção de dependência definida. A simulação não depende da renderização; o renderizador constrói um plano; o backend gráfico traduz esse plano em trabalho na GPU. O editor usa a mesma engine que inspeciona.

Minha Hibou Engine usava OpenGL, EnTT e um editor WPF. A Tessera aplica essas experiências ao controle explícito de memória e sincronização da GPU, a um ECS orientado ao escalonamento e a um editor C++ no mesmo processo. O motor é uma biblioteca estática: os símbolos D3D12SDKVersion e D3D12SDKPath do Agility SDK devem ser exportados pelo executável. Essa fronteira explícita evita uma organização frágil em DLL.

Dependências

Arquitetura em camadas

As fronteiras são impostas por pastas e interfaces. Só Platform fala com SDL3; apenas RHI/D3D12/ inclui headers Direct3D. Render decide o que desenhar sem gravar comandos na GPU. ECS, Jobs e Scene não incluem Render/ nem RHI/; Assets e Project também permanecem independentes da renderização.

Bibliotecas de importação e compressão pertencem exclusivamente a Tools/TesseraCooker. O runtime consome dados cozidos sem carregar dependências de autoria. Buscas de includes verificam essas regras, mas a compilação oferece uma prova mais forte: Tessera.NoD3D12.sln compila motor e editor com backend nulo e interface SDL_Renderer. Um tipo D3D12 que vaze quebra essa solução; seus builds produzem zero objetos Dx*.obj.

Camadas de dependência do motor e a fronteira isolada do Direct3D 12.
Camadas de dependência do motor e a fronteira isolada do Direct3D 12. Abrir original em nova aba

Simulação

ECS próprio para escalonamento

Estudei EnTT e Flecs e construí um ECS em torno de três requisitos: acesso de leitura/escrita declarado, mudanças estruturais diferidas e determinísticas e snapshots do mundo. Uma Entity é um índice e uma geração em oito bytes; a geração detecta referências obsoletas. A persistência usa um UUID, StableEntityId, em vez do handle de runtime.

Entidades com o mesmo conjunto de componentes ficam em archetypes, com colunas SoA contíguas em chunks de 64 KiB. Com dois milhões de entidades, 64 KiB exigiu 1.482 páginas contra 5.955 a 16 KiB, com a mesma largura de banda. Sparse sets guardam componentes raros sem multiplicar archetypes. Tabelas de componentes compartilhados mantêm um valor por configuração distinta, referenciado pelo chunk.

Cada SystemDescriptor declara leituras e escritas. O scheduler deriva um DAG de dependências e executa juntos os sistemas sem conflitos. Com 32 sistemas, o custo medido foi de 0,68 µs por quadro sobre 10.000 entidades e 0,86 µs sobre 1.000.000: esse custo acompanha os sistemas, não a contagem de entidades.

Fases fixas definem quando ocorrem input, simulação, física, eventos, playback estrutural, transforms e extração para render. Criar, destruir ou mudar componentes passa pelo CommandBuffer, aplicado em um único ponto determinístico. Um auditor de acesso detecta escritas não declaradas em desenvolvimento e é compilado fora em Shipping. Eventos tipados, observers e um store próprio de hierarquia completam o modelo; a propagação por versões evita recalcular nós parados.

O replay de snapshot reproduziu 120 registros de auditoria idênticos ao longo de 120 quadros na mesma máquina e build. Esse é o escopo verificado de determinismo e a base do Play/Stop do editor; não há promessa de determinismo entre plataformas.

struct Entity
{
    uint32_t Index = InvalidEntityIndex;
    uint32_t Generation = 0;
};
static_assert(sizeof(Entity) == 8,
    "Entity is a chunk column; its size is layout.");
Armazenamento de archetypes dividido em chunks com colunas contíguas de componentes.
Armazenamento de archetypes dividido em chunks com colunas contíguas de componentes. Abrir original em nova aba

Concorrência

Work stealing sem bloquear o quadro

Cada worker possui um deque Chase-Lev: empilha e desempilha sem lock, enquanto workers ociosos roubam da outra ponta. Um worker que espera um job executa outros trabalhos na própria pilha. O teste com um único worker verifica que as dependências continuam progredindo. Threads externas ao pool esperam passivamente; deixá-las ajudar criaria dois produtores para um deque que admite apenas um.

Jobs têm afinidade AnyWorker, LongRunning ou MainThread. Contadores separados acompanham trabalho do quadro e de longa duração, para a compilação de shader não prolongar a espera do quadro. Na comparação registrada, o quadro mais lento durante uma recompilação caiu de 803 ms para 18,75 ms. FrameArena por worker oferece alocação temporária; ParallelFor sobre 1.048.576 elementos produziu o resultado serial com zero alocações de heap.

Três defeitos de concorrência apareceram no desenvolvimento, incluindo um que só ocorria com a janela do editor visível. Um watchdog que nomeia o estágio travado ajudou a isolá-lo. Trocar o roubo em lote pelo roubo por elemento transferiu esse custo para a thread ociosa. Um grafo largo de trabalho real registrou 94,7% de ocupação dos workers.

O cooker usa o mesmo sistema: comprimir uma textura 2048² levou 311,8 s com uma thread e 9,7 s com 24. Tracy e uma timeline no editor expõem atividade de CPU/GPU e trabalho por worker. São medições específicas do projeto, não garantias gerais de throughput.

Timeline de profiling com jobs por worker.
Timeline de profiling com jobs por worker. Abrir original em nova aba

Persistência

Um contêiner, muitos chunks

Importar um modelo significa cozinhá-lo. Um projeto Tessera guarda formatos do motor e referencia o original por uma raiz de origem nomeada; nunca copia o original para o projeto e rejeita caminhos absolutos de origem. Assim, os dados portáveis do projeto ficam separados dos arquivos de autoria necessários à reimportação.

Os formatos binários compartilham um cabeçalho de 64 bytes com magic, versão, hash XXH3-128 e localização da tabela de chunks. Entradas FourCC localizam chunks alinhados a 256 bytes. O runtime lê apenas o mip ou LOD necessário. PROV registra caminho e hash de origem, versão do cooker e opções; varrer um projeto inalterado levou 102 ms contra 316 s para recozinhar. IDNT preserva o UUID nas reimportações.

Um .tmesh contém um asset completo com submeshes internos: o buggy transforma 109 malhas em um arquivo com 236 submeshes. SXFM guarda transforms por peça, permitindo armazenar uma roda uma vez e posicioná-la quatro vezes. No acervo medido, o armazenamento caiu de 39,9 MB para 31,0 MB e os draws de 342 para 229.

Chunks opcionais têm ausência bem definida: sem SXFM, o transform é identidade. PROV entrou sem aumentar a versão; SXFM aumentou a versão da malha, mantendo arquivos antigos abrindo e renderizando. A evolução do formato passa a ser uma decisão explícita de compatibilidade.

FormatoResponsabilidade
.tmeshAsset de malha, submeshes e LODs
.ttexTexturas e mips com compressão BC; BC6H para HDRI
.tmatMaterial OpenPBR
.tscn / .tpfb / .tcellNível, prefab e célula de streaming
.tprojDescritor de projeto em JSON, fora do contêiner binário
Layout da malha cozida e a extensão planejada para meshlets.
Layout da malha cozida e a extensão planejada para meshlets. Abrir original em nova aba

Extensão planejada

Preparando o caminho para mesh shaders

Mesh shaders não estão implementados no renderizador. O pipeline existente mantém esse caminho aberto: slots da root signature canônica são visíveis a todos os estágios, pipelines usam pipeline state stream, DXC reconhece ms e as no Shader Model 6.5 ou posterior e a detecção de capacidades consulta o suporte da GPU.

Um teste criou com sucesso um pipeline de mesh shader contra a root signature canônica. Isso verifica compatibilidade, não um caminho de renderização entregue. meshoptimizer está escolhido, mas não integrado. O plano é o cooker gerar meshlets em um novo chunk do .tmesh; arquivos antigos manterão o caminho tradicional de vértices quando o chunk estiver ausente. Os contratos do contêiner e do pipeline viabilizam essa extensão.

Fronteira da GPU

O renderizador planeja; o backend executa

IRenderBackend não expõe tipos Direct3D. Recursos usam handles com geração, como Handle<TextureTag> e Handle<MeshTag>, para detectar referências a objetos liberados. Render/ constrói um plano independente da GPU; RHI/D3D12/ o executa. O planejamento pode ser testado sem device.

O backend usa Enhanced Barriers por padrão, com fallback legacy, e bindless Shader Model 6.6 com fallback para tabelas de descritores. A root signature ocupa 13 de 64 DWORDs. Uma fila de cópia separada cuida dos uploads; streaming com orçamento de VRAM recua para mips menores sob pressão em vez de remover a cena. O hot reload de shaders reconstrói os pipelines afetados.

A gravação multithread é escolhida quando o volume de trabalho a justifica. Com 23.099 draws, o tempo de CPU do quadro caiu de 207–229 ms para 82–91 ms, ganho de 2,3–2,8×. O quadro comum e pequeno mantém uma única lista de comandos deliberadamente. O custo extra de escalonamento e gravação precisa se justificar.

O plano de render cruza uma interface abstrata antes de ser executado pelo backend D3D12.
O plano de render cruza uma interface abstrata antes de ser executado pelo backend D3D12. Abrir original em nova aba

Compilação do quadro

Um render graph que produz um plano

Um passe declara leituras e escritas de recursos, tipos de acesso e estágios da GPU. O grafo vê todo o quadro e deriva ordem e barreiras dessas declarações. Nunca grava comandos. O executor D3D12 lê a ordem de execução e as barreiras por passe e então grava o trabalho correspondente.

A compilação monta dependências contra o último escritor anterior, rejeita ciclos com um plano vazio, remove produtores sem uso, seleciona filas, calcula vidas de transitórios e gera barreiras quando o papel do recurso muda. Recursos exportados e efeitos colaterais declarados preservam trabalho necessário; apenas importar um recurso não protege seu produtor da remoção.

Ordem de uso e tempo de vida diferem com compute opcional: posições no plano não descrevem execuções sobrepostas. O grafo fornece intervalos de vida; o dono dos heaps decide aliasing e pede suas barreiras. Meu alocador empacota por vida, requisito que levou à recusa da abordagem por tamanho do D3D12 Memory Allocator avaliado. Oito recursos deliberadamente escalonados usaram 2.688 KB em vez de 10.752 KB, redução de 4,00× e o mínimo para esse teste.

O grafo forward fornecido foi exportado do Sample.tscn em 6 de outubro de 2026. Nessa configuração registrada, 20 passes produzem 15 barreiras. Seis passes desligados de oclusão/ray tracing só abrem escopos vazios: um passe sem escritas não é um produtor sem uso e não é removido automaticamente. A transição destacada de SceneColor antes de ToneMapPass é deduzida da mudança de render target para leitura de shader. As contagens variam com o caminho de shading e os efeitos ativos.

Reset() limpa os dados do quadro preservando a capacidade dos vetores; os handles de recursos do grafo expiram com ele. A exportação Graphviz e o painel do editor expõem o plano; --rendergraph-self-test executa 242 verificações sem GPU.

  1. Declarar

    Importe recursos existentes com seu estado de entrada; crie transitórios indefinidos; use AddPass para declarar Read, Write ou ReadWrite.

  2. Compilar

    Exporte os estados finais necessários, incluindo present para o backbuffer. Compile() deriva ordem de dependências, vidas de recursos e sincronização.

  3. Executar

    O backend consome GetExecutionOrder() e GetBarriers() e emite trabalho na GPU. O planejamento permanece independente de Direct3D 12.

Grafo forward exportado do Sample.tscn em 6 de outubro de 2026; a transição destacada de SceneColor é deduzida pelo grafo.
Grafo forward exportado do Sample.tscn em 6 de outubro de 2026; a transição destacada de SceneColor é deduzida pelo grafo. Abrir original em nova aba

Renderização

Iluminação com referência para comparação

O renderizador implementa coat, anisotropia e fuzz do OpenPBR, verificados por furnace test; iluminação baseada em imagem, céu procedural e HDRI; sombras em cascata e clustered light culling. No teste registrado de culling, 125× mais luzes custaram 1,39×. Forward e deferred são intercambiáveis, com SSAO, bloom, autoexposição por histograma e transparência.

DXR inline com RayQuery fornece sombras suaves, oclusão ambiente e reflexos traçados, estes limitados ao deferred. Um denoiser espaço-temporal e orçamentos adaptativos Low/High/Ultra controlam os efeitos em tempo real. Um path tracer de referência mede seu erro. As capturas mostram a cena de amostra e os diagnósticos; os contadores visíveis descrevem essas capturas, não um benchmark para toda cena.

Cena de amostra Sponza renderizada no editor Tessera.
Cena de amostra Sponza renderizada no editor Tessera. Abrir original em nova aba

Autoria

O editor usa os sistemas que expõe

Dear ImGui docking mantém o editor em C++, no processo do motor. A Lobby abre um projeto sem iniciar outro aplicativo. O Content Browser tem miniaturas renderizadas, cache em disco com invalidação automática e imports em segundo plano. Janelas de malha/prefab oferecem previews na GPU; o editor de textura isola canais e empacota ORM. As propriedades dos assets mostram diretamente os chunks binários.

Hierarchy, Inspector, transforms com ImGuizmo, undo/redo, prefabs, tags e cores por asset apoiam a edição de cenas. O Material Graph é uma visualização de leitura derivada do .tmat; sua fiação não é editável. Os widgets dos grafos vêm do GraphEditor do ImGuizmo.

Play, Pause, Stop e Simulate compartilham o mecanismo de snapshot: Stop restaura o mundo capturado e verifica seu hash idêntico. Cenas de autoria usam JSON para diffs legíveis no Git e são cozidas em binário para runtime. Profiling, Render Graph, ECS Diagnostics e Console permitem inspecionar os sistemas em execução no editor.

Editor de asset com preview 3D renderizado na GPU.
Editor de asset com preview 3D renderizado na GPU. Abrir original em nova aba

Escolhas

Dependências com papéis definidos

Escrevo as políticas específicas da engine e uso bibliotecas para problemas delimitados. SDL3 fica atrás de IPlatformBackend; DirectXMath oferece matemática SIMD em todo o motor; DXC compila HLSL Shader Model 6.x. Dear ImGui e ImGuizmo fornecem primitivas de interface, sem uma segunda stack de editor gerenciado.

cgltf, ufbx e meu importador OBJ ficam no cooker. Escolhi ufbx, open source, em vez do FBX SDK por sua triangulação; mikktspace segue as convenções de tangentes das ferramentas de arte. Entre os compressores avaliados, AMD Compressonator ofereceu a qualidade BC7 preferida. stb_image e tinyexr decodificam imagens/HDRI; xxHash identifica conteúdo cozido; nlohmann/json preserva a ordem das chaves para diffs estáveis.

Tracy fornece timelines de CPU/GPU e WinPixEventRuntime nomeia os passes da GPU no PIX. vcpkg fixa versões por baseline de manifesto. Antes de adicionar uma dependência, reviso a licença de cada componente na fonte oficial, com preferência por open source.

Validação

Builds e comparações como critérios de design

Voltei ao Sharpmake depois do CMake para um único gerador definir builds na IDE e na linha de comando. Sharpmake e vcpkg produzem duas soluções em Debug, Development, Profile e Shipping: oito builds locais, todos com avisos tratados como erros. Verificadores pós-build conferem DLLs de runtime e árvores de shaders espelhadas.

Testes por subsistema sem janela incluem 4.883 verificações do ECS, 242 do render graph e 837 do cooker. Sondas de linha de comando exercitam recursos individuais; um harness compara tempos e contadores com uma baseline. Os diffs de imagem contra a versão anterior exigem zero pixels diferentes na cena para os critérios de aceitação das refatorações descritos aqui.

Os números vêm dos registros do projeto usados neste estudo. Documentam o método de validação e os cenários medidos, sem representar uma nova execução do repositório privado por este site. O build alternativo, as auditorias de replay e as comparações de imagem verificam falhas diferentes que apenas um build normal bem-sucedido não revelaria.

Em desenvolvimento

O que ainda falta construir

Meshlets e mesh shaders são os próximos passos da renderização. Jolt Physics, miniaudio, GameNetworkingSockets e Recast & Detour são dependências escolhidas, ainda não integradas. Um servidor autoritativo para 16 jogadores é um objetivo; snapshots com escopo de replicação e uma medição de precisão para mundos de até 200 km² são a base, mas a rede ainda não foi iniciada.

Upscaling temporal, TAA e motion blur continuam como planos. Async compute está implementado, é opcional e fica desligado por padrão; o ganho medido foi de cerca de 2%, abaixo do limiar de 5% usado para justificar sua ativação. Iluminação global não está implementada. Windows é a única plataforma suportada.

Também há questões de ciclo de vida a resolver: trocar de projeto no mesmo processo ainda acumula malhas na memória da GPU. Trato isso como trabalho pendente. Estas notas tornam visíveis tanto a arquitetura implementada quanto seus limites atuais.

Contato

Cidade de Québec, QC, Canadá / Disponível para oportunidades

Estou aberto a oportunidades em desenvolvimento de software, programação de ferramentas e gameplay, além de vagas de artista 3D júnior. Se minha experiência fizer sentido para sua equipe, será um prazer conversar pelos meus perfis nas redes sociais.