Nortis (anteriormente Notris) é um jogo PSX caseiro, escrito em C usando ferramentas modernas. É totalmente jogável em hardware original e é alimentado por PSNoobSDK.
Veja a base de código PSX aqui.
No ano passado, coloquei em minhas mãos um raro PlayStation 1 preto. Ele se chama Net Yaroze e é um console especial que pode jogar jogos caseiros, bem como títulos PSX comuns. Fazia parte de um projeto especial da Sony para atrair amadores e estudantes para a indústria de jogos.
Os jogos Yaroze eram muito limitados, pois a Sony não queria que programadores de quarto competissem com desenvolvedores comerciais. Eles só podiam ser reproduzidos em outros Yarozes ou em discos demo especiais. Eles tiveram que caber inteiramente na RAM do sistema, sem acesso ao CD-ROM. Apesar dessas limitações, o Yaroze promoveu uma comunidade apaixonada de desenvolvedores independentes.
E agora eu tinha o meu próprio. O que me fez pensar: como foi realmente escrever um jogo para PlayStation?
É sobre como eu mesmo escrevi um jogo PSX homebrew simples, usando uma versão de código aberto das bibliotecas, mas ainda rodando em hardware original e escrito em C clássico.
Ignorar esta seção
Os jogos PSX eram normalmente escritos em C nas estações de trabalho Windows 9X. O devkit oficial era um par de placas de expansão ISA encaixadas em uma placa-mãe comum de PC IBM e continha todo o chipset do sistema PSX, saída de vídeo e RAM extra (8 MB em vez de 2 MB). Isso forneceu saída de TTY e depurador para a máquina host.
Você deve ter ouvido falar sobre PlayStations azuis. Eles eram para controle de qualidade e não para desenvolvimento e são idênticos às unidades de varejo, exceto que podem reproduzir CD-ROMs gravados. No entanto, pelo menos uma empresa vendeu um complemento especial para convertê-los em devkits:
O design era muito amigável ao desenvolvedor. Você pode jogar em CRT com controladores normais enquanto percorre os pontos de interrupção do GDB em seu PC com Windows 95, folheando um livro grosso de funções do C SDK.
Em princípio, um desenvolvedor PSX poderia trabalhar inteiramente em C. O SDK compreendia um conjunto de bibliotecas C chamado PSY-Q e incluía um programa compilador ccpsx
que era na verdade apenas um frontend do GCC. Isso suportou uma série de otimizações, como inlining de código e desenrolamento de loop, embora seções críticas de desempenho ainda justificassem uma montagem otimizada manualmente.
(Você pode ler sobre essas otimizações nestes slides da conferência SCEE).
C++ era suportado pelo ccpsx
, mas tinha a reputação de gerar código 'inchado', bem como tempos de compilação mais lentos. Na verdade, C era a língua franca do desenvolvimento do PSX, mas alguns projetos faziam uso de linguagens de script dinâmicas em cima de um mecanismo básico. Por exemplo, Metal Gear Solid usou TCL para scripts de nível; e os jogos Final Fantasy foram além e implementaram suas próprias linguagens de bytecode para batalhas, sistemas de campo e minijogos. (Você pode aprender mais sobre isso aqui).
( Para ler mais, dê uma olhada em https://www.retroreversing.com/official-playStation-devkit )
Ignorar esta seção
Mas cheguei a isso de uma perspectiva muito diferente: um engenheiro de software em 2024 que trabalhava principalmente em aplicações web. Minha experiência profissional foi quase exclusivamente em linguagens de alto nível como JavaScript e Haskell; Eu fiz um pouco de trabalho em OpenGL e C++, mas o C++ moderno é quase uma linguagem completamente diferente do C.
Eu sabia que existiam SDKs PSX para linguagens como Rust, mas queria experimentar o sabor da programação PSX 'real', do jeito que era feito nos anos 90. Portanto, seriam cadeias de ferramentas modernas e bibliotecas de código aberto, mas C do começo ao fim.
O jogo precisava ser algo 2D que pudesse ser prototipado em alguns dias. Decidi por um clone do Tetris - imaginei que seria complexo o suficiente para experimentar o que eu queria.
O primeiro passo foi construir um protótipo em uma tecnologia familiar. Isso me permitiria definir o design básico, então a lógica poderia ser traduzida aos poucos em C.
Como desenvolvedor web, a tecnologia mais óbvia para prototipagem era o JavaScript: é simples, conciso, fácil de depurar e possui a API gráfica HTML5 <canvas>
. As coisas aconteceram muito rapidamente
Ao mesmo tempo, eu estava preocupado com a dificuldade de portabilidade de mais recursos JavaScript de alto nível. Qualquer coisa que usasse classes ou encerramentos precisaria ser completamente reescrita, então tomei o cuidado de me restringir a um subconjunto simples e processual da linguagem.
Agora, na verdade, eu tinha um motivo oculto para assumir esse projeto: era uma desculpa para finalmente aprender C. A língua ocupava uma posição importante em minha mente e comecei a desenvolver um complexo de inferioridade por não conhecê-la.
C tem uma reputação intimidadora e eu temia histórias de terror sobre ponteiros pendurados, leituras desalinhadas e a temida segmentation fault
. Mais precisamente: eu estava preocupado que, se tentasse aprender C e falhasse, descobriria que, afinal, não era um programador muito bom.
Para facilitar as coisas, imaginei que poderia usar o SDL2 para lidar com a entrada e os gráficos e compilar para o meu ambiente de desktop (MacOS). Isso me daria um ciclo rápido de construção/depuração e tornaria a curva de aprendizado o mais suave possível.
Apesar dos meus medos, achei C incrivelmente divertido. Muito rapidamente, 'clicou' para mim. Você começa com primitivos muito simples - estruturas, caracteres, funções - e os constrói em camadas de abstração para eventualmente se encontrar no topo de um sistema inteiro em funcionamento.
O jogo levou apenas alguns dias para ser transferido e fiquei muito satisfeito com meu primeiro projeto C verdadeiro. E eu não tive uma única falha de segmento!
Foi um prazer trabalhar com SDL, mas houve alguns aspectos que exigiram que eu alocasse memória dinamicamente. Isso seria proibido no PlayStation, onde o malloc
fornecido pelo kernel do PSX não funciona corretamente. E o pipeline gráfico seria um salto ainda maior...
Quando se trata de homebrew do PlayStation, existem duas opções principais para o seu SDK. Qualquer:
Existem algumas outras opções, como o C++ Psy-Qo , e você pode até abrir mão de qualquer SDK apenas para fazer E/S mapeada em memória sozinho - mas não tive coragem suficiente para isso.
O maior problema do Psy-Q é que ele ainda é um código proprietário da Sony, mesmo 30 anos depois. Legalmente, qualquer homebrew construído com ele está em risco. Foi isso que afundou o demake do Portal64: ele vinculou estaticamente libultra
, que é o SDK N64 proprietário da Nintendo.
Mas, para ser sincero, o principal motivo pelo qual escolhi o PSNoobSDK foi porque ele é muito bem documentado e simples de configurar. A API é muito semelhante ao Psy-Q: na verdade, para muitas funções eu poderia apenas consultar as referências impressas que acompanham meu Yaroze.
Se o uso de um SDK não autêntico ofende o purista do PSX que existe em você, sinta-se à vontade para parar de ler agora, enojado.
Minha primeira tarefa foi uma espécie de olá mundo: dois quadrados sobre um fundo colorido. Parece simples, certo?
Ignorar esta seção
(* Parte disso é simplificado. Para obter um guia mais confiável, leia o tutorial PSNoobSDK)
Para começar, pense na VRAM do PSX como uma grande tela de 1024 por 512 pixels de 16 bits. Ao todo, isso perfaz 1 megabyte de memória compartilhada por framebuffers e texturas. Podemos escolher a resolução do framebuffer de saída - até 640x480 pixels se formos gananciosos - mas mais resolução = menos texturas.
A maioria dos jogos PSOne (e... jogos em geral) tem uma noção de renderização com buffer duplo: enquanto um quadro está sendo preparado, o outro é enviado para a tela. Portanto, precisamos alocar dois buffers de quadros:
(Agora você pode ver porque 640x480 não é prático - não há espaço suficiente para dois buffers de 480p. Mas este modo PODE ser usado por coisas como o logotipo de inicialização do PSX, que não precisa de muita animação)
Os buffers (referidos alternadamente como ambientes de exibição e desenho) são trocados a cada quadro. A maioria dos jogos PSX tem como alvo 30fps (na América do Norte), mas a interrupção real do VSync chega a 60Hz. Alguns jogos conseguem rodar a 60 fps - Tekken 3 e Kula World (Roll Away) vêm à mente - mas obviamente você precisa renderizar na metade do tempo. Lembre-se que temos apenas 33 MHz de poder de processamento.
Mas - como funciona o processo de desenho? Isso é feito pela GPU, mas a GPU PSX funciona de maneira muito diferente de uma placa gráfica moderna. Essencialmente, a cada quadro a GPU recebe uma lista ordenada de 'pacotes' ou comandos gráficos. "Desenhe um triângulo aqui", "carregue esta textura para esfolar o próximo quadrante", etc.
A GPU não faz transformações 3D; esse é o trabalho do coprocessador GTE (Geometry Transform Engine). Os comandos da GPU representam gráficos puramente 2D, já manipulados por hardware 3D.
Isso significa que o caminho de um pixel PSX é o seguinte:
Então, em pseudocódigo, o loop de quadros PSX (basicamente) é assim
FrameBuffer [0, 1]
OrderingTable [0, 1]
id = 1 // flips every frame
loop {
// Game logic
// Construct the next screen by populating the current ordering table
MakeGraphics(OrderingTable[id])
// Wait for last draw to finish; wait for vertical blank
DrawSync()
VSync()
// The other frame has finished drawing in background, so display it
SetDisplay(Framebuffer[!id])
// Start drawing current frame
SetDrawing(Framebuffer[id])
// Send ordering table contents to GPU via DMA
Transfer(OrderingTable[id])
// Flip
id = !id
}
Você pode ver que enquanto o quadro 1 está na tela, o quadro 2 ainda está sendo pintado e o quadro 3 ainda está potencialmente sendo 'construído' pelo próprio programa. Então, após DrawSync / VSync, enviamos o quadro 2 para a TV e obtemos o quadro de desenho 3 da GPU.
Como mencionado, a GPU é uma peça de hardware totalmente 2D, ela não conhece as coordenadas z no espaço 3D. Não existe um "z-buffer" para descrever oclusões - ou seja, quais objetos estão na frente de outros. Então, como os itens são classificados na frente dos outros?
A forma como funciona é que a tabela de pedidos compreende uma cadeia reversa de comandos gráficos. Eles são percorridos de trás para frente para implementar o algoritmo do pintor .
Para ser mais preciso, a tabela de pedidos é uma lista vinculada inversamente. Cada item possui um ponteiro para o item anterior da lista e adicionamos primitivos inserindo-os na cadeia. Geralmente os OTs são inicializados como uma matriz fixa, com cada elemento da matriz representando um 'nível' ou camada na exibição. Os OTs podem ser aninhados para implementar cenas complexas.
O diagrama a seguir ajuda a explicar isso (fonte)
Esta abordagem não é perfeita e às vezes a geometria do PSX mostra recortes estranhos, porque cada polígono só pode estar em um único 'índice z' no espaço da tela, mas funciona bem o suficiente para a maioria dos jogos. Hoje em dia, tais limitações são consideradas parte do charme distintivo do PSX.
Ignorar esta seção
Conversamos muito sobre teoria – como é isso na prática?
Esta seção não abordará todo o código linha por linha, mas deve fornecer uma amostra dos conceitos gráficos do PSX. Se você quiser ver o código completo, vá para hello-psx/main.c
.
Alternativamente, se você não for um programador, sinta-se à vontade para avançar. Isto é apenas para técnicos curiosos.
A primeira coisa que precisamos são de algumas estruturas para conter nossos buffers. Teremos um RenderContext
que contém dois RenderBuffers
e cada RenderBuffer
conterá:
displayEnv
(especifica a área VRAM do buffer de exibição atual)drawEnv
(especifica a área VRAM do buffer de desenho atual)orderingTable
(lista vinculada reversa que conterá ponteiros para pacotes gráficos)primitivesBuffer
(estruturas para pacotes/comandos gráficos - incluindo todos os polígonos) #define OT_SIZE 16
#define PACKETS_SIZE 20480
typedef struct {
DISPENV displayEnv ;
DRAWENV drawEnv ;
uint32_t orderingTable [ OT_SIZE ];
uint8_t primitivesBuffer [ PACKETS_SIZE ];
} RenderBuffer ;
typedef struct {
int bufferID ;
uint8_t * p_primitive ; // next primitive
RenderBuffer buffers [ 2 ];
} RenderContext ;
static RenderContext ctx = { 0 };
A cada quadro, inverteremos o bufferID
, o que significa que podemos trabalhar perfeitamente em um quadro enquanto o outro está sendo exibido. Um detalhe importante é que o p_primitive
é constantemente mantido apontado para o próximo byte no primitivesBuffer
atual. É imperativo que isso seja incrementado toda vez que uma primitiva for alocada e redefinida no final de cada quadro.
Praticamente antes de qualquer coisa precisamos configurar nossos ambientes de exibição e desenho, em configuração reversa para que DISP_ENV_1
use a mesma VRAM que DRAW_ENV_0
e vice-versa
// x y width height
SetDefDispEnv ( DISP_ENV_0 , 0 , 0 , 320 , 240 );
SetDefDispEnv ( DISP_ENV_1 , 0 , 240 , 320 , 240 );
SetDefDrawEnv ( DRAW_ENV_0 , 0 , 240 , 320 , 240 );
SetDefDrawEnv ( DRAW_ENV_1 , 0 , 0 , 320 , 240 );
Estou sendo bastante condensado aqui - mas a partir daqui cada quadro basicamente é como
while ( 1 ) {
// do game stuff... create graphics for next frame...
// at the end of loop body
// wait for drawing to finish, wait for next vblank interval
DrawSync ( 0 );
VSync ( 0 );
DISPENV * p_dispenv = & ( ctx . buffers [ ctx . bufferID ]. displayEnv );
DRAWENV * p_drawenv = & ( ctx . buffers [ ctx . bufferID ]. drawEnv );
uint32_t * p_ordertable = ctx . buffers [ ctx . bufferID ]. orderingTable ;
// Set display and draw environments
PutDispEnv ( p_dispenv );
PutDrawEnv ( p_drawenv );
// Send ordering table commands to GPU via DMA, starting from the end of the table
DrawOTagEnv ( p_ordertable + OT_SIZE - 1 , p_drawEnv );
// Swap buffers and clear state for next frame
ctx . bufferID ^= 1 ;
ctx . p_primitive = ctx . buffers [ ctx . bufferID ]. primitivesBuffer ;
ClearOTagR ( ctx . buffers [ 0 ]. orderingTable , OT_SIZE );
}
Isso pode ser muito para absorver. Não se preocupe.
Se você realmente quer entender isso, o melhor é dar uma olhada em hello-psx/main.c
. Tudo é comentado com bastante detalhe. Alternativamente, siga o tutorial PSNoobSDK... é bastante conciso e escrito de forma bastante clara.
Agora... como desenhamos coisas? Escrevemos estruturas em nosso buffer primitivo. Este buffer é digitado apenas como uma grande lista de chars
, então lançamos em nossa estrutura de forma/comando e, em seguida, avançamos o ponteiro do buffer primitivo usando sizeof
:
// Create a tile primitive in the primitive buffer
// We cast p_primitive as a TILE*, so that its char used as the head of the TILE struct
TILE * p_tile = ( TILE * ) p_primitive ;
setTile ( p_tile ); // very very important to call this macro
setXY0 ( p_tile , x , y );
setWH ( p_tile , width , width );
setRGB0 ( p_tile , 252 , 32 , 3 );
// Link into ordering table (z level 2)
int z = 2 ;
addPrim ( ordering_table [ buffer_id ] + z , p_primitive );
// Then advance buffer
ctx . p_primitive += sizeof ( TILE );
Acabamos de inserir um quadrado amarelo! ? Tente conter sua excitação.
Ignorar esta seção
Neste ponto da minha jornada, tudo o que eu realmente tinha era um programa de demonstração "olá mundo", com gráficos básicos e entrada de controlador. Você pode ver no código em hello-psx
que eu estava documentando o máximo possível, na verdade para meu próprio benefício. Um programa de trabalho foi um passo positivo, mas não um verdadeiro jogo.
Era hora de cair na real .
Nosso jogo precisa mostrar o placar.
O PSX realmente não oferece muito em termos de renderização de texto. Existe uma fonte de depuração (mostrada acima), mas é extremamente básica - para desenvolvimento e não muito mais.
Em vez disso, precisamos criar uma textura de fonte e usá-la para criar quadrantes. Criei uma fonte monoespaçada com https://www.piskelapp.com/ e exportei como um PNG transparente:
As texturas PSX são armazenadas em um formato chamado TIM. Cada arquivo TIM compreende:
Como a localização VRAM da textura está “incorporada” ao arquivo TIM, você precisa de uma ferramenta para gerenciar a localização da textura. Eu recomendo https://github.com/Lameguy64/TIMedit para isso.
A partir daí, temos apenas uma função para esfolar vários quadrantes, com os deslocamentos UV baseados em cada valor ASCII.
Precisamos de um espaço para as peças caberem. Seria fácil usar um retângulo branco chato para isso, mas eu queria algo que parecesse mais... PlayStation
Nossa interface de usuário está se unindo. E as peças?
Agora vem um design visual importante. Idealmente, cada tijolo deve ser visualmente distinto, com bordas nítidas e sombreadas. Fazemos isso com dois triângulos e um quádruplo:
Na resolução nativa de 1x, o efeito seria menos claro, mas ainda parece bonito e robusto:
No primeiro protótipo do meu jogo eu implementei um sistema de rotação totalmente ingênuo, que na verdade viraria o bloco 90 graus em um ponto central. Acontece que essa não é realmente uma ótima abordagem, porque faz com que os blocos 'oscilem', movendo-se para cima e para baixo à medida que giram:
Em vez disso, as rotações são codificadas para serem 'legais' em vez de 'precisas'. Uma Peça é definida dentro de uma grade de células 4x4, e cada célula pode ser preenchida ou não. Existem 4 rotações. Portanto: as rotações podem ser apenas matrizes de quatro números de 16 bits. Que se parece com isto:
/**
* Example: T block
*
* As a grid:
*
* .X.. -> 0100
* XXX. -> 1110
* .... -> 0000
* .... -> 0000
*
* binary = 0b0100111000000000
* hexadecimal = 0x4E00
*
*/
typedef int16_t ShapeBits ;
static ShapeBits shapeHexes [ 8 ][ 4 ] = {
{ 0 }, // NONE
{ 0x0F00 , 0x4444 , 0x0F00 , 0x4444 }, // I
{ 0xE200 , 0x44C0 , 0x8E00 , 0xC880 }, // J
{ 0xE800 , 0xC440 , 0x2E00 , 0x88C0 }, // L
{ 0xCC00 , 0xCC00 , 0xCC00 , 0xCC00 }, // O
{ 0x6C00 , 0x8C40 , 0x6C00 , 0x8C40 }, // S
{ 0x0E40 , 0x4C40 , 0x4E00 , 0x4640 }, // T
{ 0x4C80 , 0xC600 , 0x4C80 , 0xC600 }, // Z
};
Extrair os valores das células é apenas um caso de simples mascaramento de bits:
#define GRID_BIT_OFFSET 0x8000;
int blocks_getShapeBit ( ShapeBits s , int y , int x ) {
int mask = GRID_BIT_OFFSET >> (( y * 4 ) + x );
return s & mask ;
}
As coisas estão se unindo agora com impulso.
Foi nesse ponto que encontrei um obstáculo: a randomização. As peças têm que aparecer de forma aleatória para que valha a pena jogar o jogo, mas a randomização é difícil com computadores. Na minha versão MacOS, consegui 'propagar' o gerador de números aleatórios com o relógio do sistema, mas o PSX não tem relógio interno.
Em vez disso, uma solução que muitos jogos adotam é fazer com que o jogador crie a semente. O jogo exibe uma tela inicial ou de título com texto como 'pressione iniciar para começar' e, em seguida, o tempo é obtido a partir do pressionamento do botão para criar a semente.
Eu criei um 'gráfico' declarando alguns int32
s codificados em binário onde 1
bit seria um 'pixel' em uma linha de tijolos:
O que eu queria era que as linhas se dissolvessem gradualmente à vista. Primeiro, eu precisava de uma função que efetivamente 'monitorasse' quantas vezes ela foi chamada. C facilita isso com a palavra-chave static
- se usada dentro de uma função, o mesmo endereço de memória e conteúdo são reutilizados na próxima invocação.
Então, dentro desta mesma função há um loop que percorre os valores x/y da 'grade' e decide se ocorreram ticks suficientes para mostrar o 'pixel':
void ui_renderTitleScreen () {
static int32_t titleTimer = 0 ;
titleTimer ++ ;
// For every 2 times (2 frames) this function is called, ticks increases by 1
int32_t ticks = titleTimer / 2 ;
// Dissolve-in the title blocks
for ( int y = 0 ; y < 5 ; y ++ ) {
for ( int x = 0 ; x < 22 ; x ++ ) {
int matrixPosition = ( y * 22 ) + x ;
if ( matrixPosition > ticks ) {
break ; // because this 'pixel' of the display is not to be displayed yet
}
int32_t titleLine = titlePattern [ y ];
int32_t bitMask = titleMask >> x ;
if ( titleLine & bitMask ) { // there is a 'pixel' at this location to show
ui_renderBlock ( /* skip boring details */ );
}
}
}
}
Estamos quase lá agora.
Os jogos clássicos do PSX inicializam em dois estágios: primeiro a tela da Sony Computer Entertainment e depois o logotipo do PSX. Mas se compilarmos e executarmos o projeto hello-psx
isso não acontece. A segunda tela é apenas preta. Por que é que?
Bem, o splash do SCE vem do BIOS, assim como o som de inicialização do PSX, mas o famoso logotipo faz parte dos dados de licença do disco. Ele existe para funcionar como um ‘selo de autenticidade’ – então qualquer pessoa que pirateie um jogo estará copiando a propriedade intelectual da Sony e também da editora. Isto deu à Sony mais instrumentos legais para reprimir a pirataria de software.
Se quisermos que nosso jogo mostre o logotipo, precisamos fornecer um arquivo de licença extraído de uma ISO, mas por questão de direitos autorais temos que .gitignore
-lo.
< license file = " ${PROJECT_SOURCE_DIR}/license_data.dat " />
OK. Agora estamos prontos.
Tudo isso começou com uma compra por impulso, meu Yaroze PlayStation preto. Ironicamente, ele não estaria jogando meu jogo, pois ainda possuía seu hardware antipirataria. Eu não gostaria de instalar um modchip em uma peça tão inestimável da história do PSX – não com minhas habilidades de soldagem.
Em vez disso, tive que rastrear um PlayStation cinza modificado, que ainda tivesse uma direção decente. Achei que o objetivo do meu projeto era escrever um verdadeiro jogo de PlayStation e isso significava usar um verdadeiro PlayStation.
Eu também tive que encontrar a mídia certa. O laser PSX é bastante exigente e os CD-Rs modernos tendem a ser muito menos refletivos do que os discos prensados. Minhas primeiras tentativas com CDs de histórias de supermercado foram uma perda de tempo e, no espaço de cerca de duas semanas, criei muitos porta-copos.
Este foi um momento sombrio. Eu tinha chegado até aqui e falhado na gravação do CD ?
Depois de várias semanas, consegui algumas ações especiais da JVC Taiyo Yuden. Pelo que pude ler, eles eram bastante especializados e normalmente usados em aplicações industriais. Gravei o primeiro disco da bandeja e esperava o pior.
Este foi o momento da verdade:
A sequência de inicialização do PlayStation explodiu nos minúsculos alto-falantes do monitor e o clássico logotipo “PS” apareceu na tela em uma resolução vibrante de 640 por 480. O BIOS claramente encontrou algo naquele disco, mas muita coisa pode falhar após esse ponto. A tela ficou preta e eu esforcei meus ouvidos para ouvir o clique-clique-clique revelador de um erro de unidade.
Em vez disso, um por um, pequenos quadrados coloridos começaram a piscar na escuridão. Linha por linha, eles soletraram uma palavra: NOTRIS
. Então: PRESS START TO BEGIN
. O texto me chamou a atenção. O que aconteceria a seguir?
Um jogo de Tetris, claro. Por que fiquei surpreso? Escrever seu próprio jogo de PlayStation em C é na verdade muito simples: tudo o que é necessário é não cometer nenhum erro . Isso é computação para você, especialmente as coisas de baixo nível. É difícil, afiado e lindo. A computação moderna tem arestas mais suaves, mas o essencial não mudou.
Aqueles de nós que amam computadores precisam ter algo ligeiramente errado conosco, uma irracionalidade em nossa racionalidade, uma forma de negar todas as evidências de nossos olhos e ouvidos de que a caixa hostil de silício está morta e inflexível. E forme, por meio de maquinaria astuta, a ilusão de que ela vive.