Tudo começa quando você decide fazer o seu próprio Sistema Operacional. Você passa pelo bootloader, entra no modo protegido, modo longo, e finalmente consegue desenhar um pixel na tela usando VESA BIOS Extensions (VBE).

Você comemora.

Aí depois tenta limpar a tela inteira com uma cor sólida e percebe que está rodando a incríveis 5 FPS.

Lentidao

E a pergunta é… Mas por quê?

Então nós começamos. O VBE basicamente te dá um endereço físico da memória de vídeo, o Framebuffer. É um ponteiro. Você escreve as cores lá e a GPU mostra na tela.

Simples, né?

Não.

O problema é que esse ponteiro aponta para a memória de vídeo da placa. Então quando você faz:

// O jeito ingênuo
uint32_t* fb = (uint32_t*)graphicsManager.vbe.framebuffer_addr;
for (int i = 0; i < width * height; i++) {
    fb[i] = cor;
}

você está escrevendo diretamente numa região de memória que não funciona como uma RAM normal.

E isso é horrivelmente lento.

Dependendo de como essa região está configurada, cada escrita pode acabar passando pelo caminho de memória até a placa de vídeo sem que a CPU consiga simplesmente tratar aquilo como uma região normal de RAM cacheável.

Então como fazemos o CPU Rendering ficar rápido sem ter que escrever um driver de GPU diferente para cada placa de vídeo existente?

Aqui entra uma coisa muito útil: MTRR (Memory Type Range Registers) e Write-Combining.

A CPU possui registradores que permitem definir como determinadas regiões da memória física devem ser tratadas.

Normalmente, o framebuffer é mapeado como Uncacheable (UC). Ou seja, você não quer que a CPU fique tratando aquela memória como uma RAM normal e colocando tudo nas caches.

Só que existe outro tipo: Write-Combining (WC).

Com WC, a CPU pode juntar várias escritas pequenas antes de mandar os dados para o dispositivo. Em vez de ficar fazendo uma escrita atrás da outra no barramento, ela consegue combinar essas operações em transferências maiores.

E isso faz uma diferença absurda para framebuffer.

Então no ViniciusOS nós podemos procurar um MTRR livre, calcular a máscara correspondente ao tamanho do framebuffer e configurar a região como Write-Combining usando wrmsr.

void force_mtrr_write_combining(uint64_t phys_addr, uint64_t size) {
    // 1. Find an empty MTRR slot (Valid bit == 0)
    int free_mtrr = -1;
    for (int i = 0; i < 8; i++) {
        uint64_t msr_val = rdmsr(0x201 + (i * 2));
        if ((msr_val & (1 << 11)) == 0) {
            free_mtrr = i;
            break;
        }
    }

    if (free_mtrr == -1) {
        serial_printf("[MTRR] FATAL: No free variable MTRRs found!\n");
        return;
    }

    // 2. Round size up to next power of 2
    uint64_t p2_size = 1;
    while (p2_size < size) p2_size <<= 1;

    // 3. Get MAXPHYADDR to prevent reserved bit faults
    uint32_t eax, ebx, ecx, edx;
    asm volatile("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) : "a"(0x80000008));
    uint8_t maxphyaddr = eax & 0xFF;
    uint64_t valid_address_mask = (1ULL << maxphyaddr) - 1;

    // Calculate strict contiguous mask
    uint64_t mask = ~(p2_size - 1) & valid_address_mask;
    uint32_t base_msr = 0x200 + (free_mtrr * 2);
    uint32_t mask_msr = 0x201 + (free_mtrr * 2);

    // ========================================================
    // STRICT INTEL CACHE DISABLE SEQUENCE
    // ========================================================
    asm volatile("cli");
    uint64_t cr0;
    asm volatile("mov %%cr0, %0" : "=r"(cr0));

    // Set Cache Disable (CD) and clear Not-Writethrough (NW)
    uint64_t cr0_disable_cache = cr0 | (1 << 30);
    cr0_disable_cache &= ~(1 << 29);
    asm volatile("mov %0, %%cr0" :: "r"(cr0_disable_cache));

    asm volatile("wbinvd");

    uint64_t base_val = (phys_addr & ~0xFFFULL) | 0x01ULL;
    wrmsr(base_msr, base_val);

    uint64_t mask_val = mask | (1ULL << 11);
    wrmsr(mask_msr, mask_val);

    asm volatile("wbinvd");
    asm volatile("mov %0, %%cr0" :: "r"(cr0));
    // asm volatile("sti");

    serial_printf("[MTRR] Write-Combining active on MTRR %d!\n", free_mtrr);
}

Isso já resolve uma parte enorme do problema.

Mas no ViniciusOS nós não desenhamos diretamente no framebuffer o tempo inteiro.

Nós usamos Double Buffering.

Ou seja, existe um buffer na RAM normal onde o CPU faz todo o rendering. Quando terminamos de desenhar o frame, copiamos o buffer inteiro para o framebuffer.

Fica mais ou menos assim:

CPU
Back Buffer (RAM)
copia inteira
Framebuffer (VRAM)
Tela

E aqui aparece outro problema.

Você pode simplesmente usar memcpy(). Pode usar rep movsb. Funciona.

Mas nós estamos tentando arrancar o máximo possível de performance disso, então podemos usar AVX e Non-Temporal Stores.

O AVX nos dá os registradores YMM de 256 bits, então conseguimos carregar 32 bytes por instrução.

E com vmovntdq, a ideia é fazer uma escrita Non-Temporal. A CPU não precisa colocar esses dados nas caches como faria com uma escrita normal, porque nós basicamente sabemos que esses pixels não vão ser lidos imediatamente pela CPU de novo.

Isso combina muito bem com o framebuffer configurado como Write-Combining.

Então a cópia fica assim:

__attribute__((target("avx")))
void fast_framebuffer_copy(void *dest, const void *src, uint32_t size_in_bytes) {
    // Process 256 bytes (8 YMM registers) per loop iteration
    uint32_t chunks = size_in_bytes / 256;
    uint32_t remainder = size_in_bytes % 256;

    uint8_t *d = (uint8_t *)dest;
    const uint8_t *s = (const uint8_t *)src;

    if (chunks > 0) {
        asm volatile (
            "1:\n\t"
            // Puxa a memória do Back Buffer antes de precisarmos dela.
            "prefetcht0 512(%1)\n\t"

            // READ 256 bytes from System RAM
            "vmovdqu 0(%1), %%ymm0\n\t"
            "vmovdqu 32(%1), %%ymm1\n\t"
            "vmovdqu 64(%1), %%ymm2\n\t"
            "vmovdqu 96(%1), %%ymm3\n\t"
            "vmovdqu 128(%1), %%ymm4\n\t"
            "vmovdqu 160(%1), %%ymm5\n\t"
            "vmovdqu 192(%1), %%ymm6\n\t"
            "vmovdqu 224(%1), %%ymm7\n\t"

            // WRITE 256 bytes
            "vmovntdq %%ymm0, 0(%0)\n\t"
            "vmovntdq %%ymm1, 32(%0)\n\t"
            "vmovntdq %%ymm2, 64(%0)\n\t"
            "vmovntdq %%ymm3, 96(%0)\n\t"
            "vmovntdq %%ymm4, 128(%0)\n\t"
            "vmovntdq %%ymm5, 160(%0)\n\t"
            "vmovntdq %%ymm6, 192(%0)\n\t"
            "vmovntdq %%ymm7, 224(%0)\n\t"

            "add $256, %0\n\t"
            "add $256, %1\n\t"

            "dec %2\n\t"
            "jnz 1b\n\t"
            : "+r"(d), "+r"(s), "+r"(chunks)
            :
            : "ymm0", "ymm1", "ymm2", "ymm3",
              "ymm4", "ymm5", "ymm6", "ymm7", "memory"
        );
    }

    // Handle the remaining bytes
    if (remainder > 0) {
        asm volatile ("rep movsb"
            : "+D"(d), "+S"(s), "+c"(remainder)
            :
            : "memory");
    }

    // asm volatile("sfence" ::: "memory");
    asm volatile("vzeroupper" ::: "memory");
}

O prefetcht0 aqui serve para tentar puxar os dados do Back Buffer para a cache antes de precisarmos deles. Afinal, estamos lendo RAM normal, então faz sentido deixar a CPU preparar esses dados enquanto continua o trabalho.

E nós processamos 256 bytes por iteração usando os oito registradores YMM.

Então em vez de ficar fazendo uma cópia pequena atrás da outra, a CPU fica carregando uma quantidade grande de pixels, mandando para o framebuffer e repetindo.

E é basicamente assim que você consegue fazer CPU Rendering funcionar decentemente em um Sistema Operacional recém-nascido.

Você não precisa ter uma GPU 3D programável.

Você não precisa escrever um driver monstruoso para cada GPU existente.

Você tem um framebuffer, RAM normal, algumas instruções SIMD e uma CPU fazendo o trabalho pesado.

No fim, o que antes estava rodando a 5 FPS passa a conseguir manter 60 FPS estáveis na resolução nativa.

Nada mal para um sistema operacional que mal nasceu.