DLSS no Windows nativo vs Linux + Proton: desempenho e consumo de recursos

Introdução

O problema

Jogar no Linux deixou de ser exceção. Com o Proton, a camada de compatibilidade da Valve, jogos feitos para Windows rodam no Linux sem nenhuma alteração: as chamadas de DirectX 12 são traduzidas, em tempo real, para Vulkan. A pergunta que fica é quanto custa essa tradução.

Comparações entre plataformas costumam se basear em sessões de poucos minutos e olhar só para o FPS médio. Esse retrato é limitado: alguns minutos não bastam para a temperatura e o clock da GPU se estabilizarem, e a média sozinha esconde as travadas e a variação que o jogador sente na prática. Além disso, a diferença entre as plataformas não está só no desempenho: a camada de tradução também pode mudar quanta memória, VRAM, CPU e energia o mesmo jogo consome.

Este relatório responde à seguinte pergunta: **com o mesmo hardware, a mesma cena e a mesma configuração de DLSS, como o desempenho e o consumo de recursos se comportam ao longo de 8 horas no Windows nativo e no Linux com Proton?

Abordagem

Os dados vêm de um experimento que eu mesmo conduzi: uma cena do Unreal Engine 5 com DLSS (modo Quality) e ray tracing ativos rodou em loop por 8 horas em cada plataforma, na mesma máquina (i7-7700K, RTX 3070 Ti, 16 GB de RAM). Um script registrou 24 métricas a cada 5 segundos, de FPS e tempo de frame a memória do processo, VRAM, temperatura e clock da GPU, totalizando mais de 11 mil amostras. Um segundo registro, a cada minuto, acompanhou os outros processos ativos no sistema.

A análise segue três etapas:

  1. Preparação: juntar as bases das duas plataformas, padronizar fusos horários e tipos de dados, e remover o período de aquecimento, em que os valores ainda não se estabilizaram.
  2. Comparação: confrontar as plataformas não só pela média, mas pela estabilidade (variabilidade e travadas) e pela eficiência (FPS por watt).
  3. Tendência ao longo do tempo: dividir as 8 horas em blocos e verificar se o desempenho cai ou se o uso de memória e de recursos cresce sem parar, que é o sinal característico do envelhecimento de software.

A quem esta análise interessa

  • Jogadores que estão decidindo entre Windows e Linux saberão o que esperar em sessões longas, não só nos primeiros minutos.
  • Desenvolvedores de jogos e do Proton terão indícios de onde a camada de tradução consome mais recursos.
  • Pesquisadores de confiabilidade de software terão um exemplo de como medir envelhecimento num ambiente de uso real, com dados abertos e reproduzíveis.*

Pacotes requeridos

# Para instalar tudo de uma vez (rodar só uma vez no console, NÃO aqui no Rmd):
# install.packages(c("tidyverse", "lubridate", "janitor", "jsonlite", "slider",
#                    "plotly", "DT", "knitr", "kableExtra", "rmdformats"))

library(tidyverse)   # readr (importar CSV), dplyr (manipular), tidyr (pivotar) e ggplot2 (gráficos)
library(lubridate)   # converter os timestamps de UTC para o horário de Recife
library(janitor)     # padronizar nomes de colunas e checar colunas vazias
library(jsonlite)    # ler os metadados das sessões (arquivos .json)
library(slider)      # médias móveis para suavizar as séries de 5 em 5 segundos
library(plotly)      # gráficos interativos com zoom ao longo das 8 h
library(DT)          # tabelas interativas (busca e paginação)
library(knitr)       # tabelas estáticas com kable()
library(kableExtra)  # formatação das tabelas do kable (cores, destaques)

Preparação dos dados

Fonte dos dados

(Item 3.1 e 3.2: citar o repositório com hiperlink e explicar a origem dos dados. Dá para reaproveitar o README.)

Importação

Os dados brutos estão no repositório público Jlucas66/Projeto_2VA_Dados. Cada plataforma tem uma pasta própria, e dentro dela há três arquivos usados nesta análise: as métricas coletadas a cada 5 s, os processos do sistema coletados a cada ~60 s e os metadados da sessão em JSON. Os arquivos são lidos diretamente da web, para que o relatório possa ser reproduzido em qualquer computador.

# Parte comum de todas as URLs ("raw" = arquivo puro, sem a página do GitHub em volta)
base_url <- "https://raw.githubusercontent.com/Jlucas66/Projeto_2VA_Dados/main"

# Pasta de cada plataforma no repositório
pasta_linux   <- "2026-10-31_12-31_linux"
pasta_windows <- "2026-11-02_14-50_windows"

# Função auxiliar: monta a URL completa de um arquivo a partir da pasta e do nome.
# Evita repetir a URL inteira seis vezes.
url_repo <- function(pasta, arquivo) {
  paste(base_url, pasta, arquivo, sep = "/")
}

# Métricas principais (uma linha a cada ~5 s)
metricas_linux   <- read_csv(url_repo(pasta_linux,   "02_dlss_linux.csv"),
                             show_col_types = FALSE)
metricas_windows <- read_csv(url_repo(pasta_windows, "02_dlss_windows.csv"),
                             show_col_types = FALSE)

# Processos do sistema (uma linha por processo, a cada ~60 s)
processos_linux   <- read_csv(url_repo(pasta_linux,   "02_dlss_processos_linux.csv"),
                              show_col_types = FALSE)
processos_windows <- read_csv(url_repo(pasta_windows, "02_dlss_processos_windows.csv"),
                              show_col_types = FALSE)

# Metadados das sessões (máquina, versões, configuração do teste)
sessao_linux   <- fromJSON(url_repo(pasta_linux,   "sessao.json"))
sessao_windows <- fromJSON(url_repo(pasta_windows, "sessao_windows.json"))

Visão geral do que foi importado:

# Lista nomeada com as quatro tabelas, para resumir todas de uma vez
bases <- list(
  "Métricas - Linux"    = metricas_linux,
  "Métricas - Windows"  = metricas_windows,
  "Processos - Linux"   = processos_linux,
  "Processos - Windows" = processos_windows
)

# map_dfr aplica a função a cada base e empilha os resultados numa tabela só
map_dfr(bases, \(b) tibble(linhas = nrow(b), colunas = ncol(b)), .id = "base") |>
  kable(caption = "Bases importadas") |>
  kable_styling(full_width = FALSE)
Bases importadas
base linhas colunas
Métricas - Linux 5759 25
Métricas - Windows 5756 25
Processos - Linux 1439 6
Processos - Windows 485 6
# Estrutura da base principal: nome, tipo e primeiros valores de cada coluna
glimpse(metricas_linux)
## Rows: 5,759
## Columns: 25
## $ timestamp_utc      <dttm> 2026-10-31 15:31:54, 2026-10-31 15:31:59, 2026-10-…
## $ horas              <dbl> 0.00638, 0.00776, 0.00916, 0.01055, 0.01193, 0.0133…
## $ execucao           <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
## $ fps                <dbl> 16.60, 25.72, 30.11, 26.57, 26.50, 25.68, 25.52, 25…
## $ fps_exibido        <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ frametime_p99_ms   <dbl> 730.127, 72.052, 58.570, 61.369, 55.475, 46.835, 46…
## $ gpu_time_ms        <dbl> 30.638, 18.863, 20.551, 22.952, 27.826, 23.015, 26.…
## $ game_thread_ms     <dbl> 4.569, 4.321, 5.957, 4.978, 9.440, 6.873, 3.436, 2.…
## $ render_thread_ms   <dbl> 33.484, 25.842, 27.935, 31.238, 32.530, 29.189, 23.…
## $ proc_mem_mb        <dbl> 2188.1, 2225.2, 2293.0, 2223.4, 2310.5, 2235.1, 223…
## $ proc_private_mb    <dbl> 1335.5, 1348.6, 1352.5, 1362.9, 1365.9, 1370.5, 137…
## $ proc_threads       <dbl> 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70,…
## $ proc_handles       <dbl> 365, 365, 365, 365, 365, 365, 365, 365, 365, 365, 3…
## $ proc_cpu_pct       <dbl> NA, 50.3, 51.5, 48.9, 50.4, 50.9, 48.0, 44.7, 44.4,…
## $ cpu_total_pct      <dbl> 0.0, 52.9, 54.1, 51.3, 52.5, 53.1, 49.6, 46.2, 46.4…
## $ cpu_nucleo_max_pct <dbl> 0.0, 53.6, 57.1, 54.0, 53.6, 55.4, 52.3, 48.2, 49.5…
## $ outros_cpu_pct     <dbl> NA, 2.7, 2.5, 2.4, 2.1, 2.2, 1.6, 1.5, 2.0, 2.3, 1.…
## $ ram_mb             <dbl> 4138.8, 4111.5, 4078.1, 4043.2, 4064.4, 4032.0, 401…
## $ vram_mb            <dbl> 3912.0, 4045.9, 4155.2, 4123.1, 4235.2, 4235.1, 429…
## $ gpu_util_pct       <dbl> 71, 57, 60, 67, 67, 71, 80, 66, 75, 75, 74, 73, 65,…
## $ gpu_temp_c         <dbl> 67, 67, 67, 71, 74, 74, 76, 77, 78, 78, 80, 80, 77,…
## $ gpu_clock_mhz      <dbl> 1905, 1920, 1920, 1920, 1905, 1905, 1890, 1905, 186…
## $ gpu_power_w        <dbl> 231.1, 192.0, 190.5, 208.2, 222.4, 218.1, 238.6, 23…
## $ throttle_power     <dbl> 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, …
## $ throttle_thermal   <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, …

Limpeza da base de métricas

Junção das plataformas

As duas bases de métricas têm exatamente as mesmas 25 colunas, então podem ser empilhadas numa só. Isso permite escrever cada passo da limpeza uma única vez, em vez de repeti-lo para cada plataforma. Uma nova coluna, plataforma, guarda a origem de cada linha, já que essa informação estava só no nome do arquivo.

metricas <- bind_rows(
  "Linux + Proton" = metricas_linux,
  "Windows"        = metricas_windows,
  .id = "plataforma"   # o nome de cada item da lista vira o valor da nova coluna
)

Valores ausentes

Antes de remover qualquer coisa, verificamos quais colunas têm valores ausentes e em que proporção.

metricas |>
  summarise(across(everything(), \(x) sum(is.na(x)))) |>    # conta NAs em cada coluna
  pivot_longer(everything(), names_to = "coluna",
               values_to = "ausentes") |>                    # largo -> longo: uma linha por coluna
  filter(ausentes > 0) |>                                    # só as colunas com algum NA
  mutate(percentual = round(100 * ausentes / nrow(metricas), 2)) |>
  kable(caption = "Colunas com valores ausentes") |>
  kable_styling(full_width = FALSE)
Colunas com valores ausentes
coluna ausentes percentual
fps_exibido 11515 1e+02
proc_cpu_pct 3 3e-02
outros_cpu_pct 3 3e-02

A coluna fps_exibido está 100% vazia: ela só é preenchida quando o Frame Generation está ligado, o que não acontece neste teste. Por isso, ela é removida. Já proc_cpu_pct e outros_cpu_pct têm apenas 3 valores ausentes, um na primeira amostra de cada execução do jogo, porque o uso de CPU é calculado pela diferença entre duas leituras. Essas linhas serão tratadas junto com o período de aquecimento.

Fuso horário e tipos das variáveis

metricas <- metricas |>
  remove_empty(which = "cols") |>                        # janitor: remove colunas 100% vazias
  mutate(
    data_hora  = with_tz(timestamp_utc, tzone = "America/Recife"),  # UTC -> horário local
    plataforma = factor(plataforma),                     # categoria, não texto livre
    execucao   = factor(execucao),                       # 1, 2... é um rótulo, não uma quantidade
    across(c(throttle_power, throttle_thermal), as.logical),  # 0/1 -> FALSE/TRUE
    across(c(proc_threads, proc_handles), as.integer)    # contagens são números inteiros
  ) |>
  select(plataforma, execucao, data_hora, horas,         # identificação primeiro
         everything(), -timestamp_utc)                   # depois o resto; descarta o UTC

glimpse(metricas)
## Rows: 11,515
## Columns: 25
## $ plataforma         <fct> Linux + Proton, Linux + Proton, Linux + Proton, Lin…
## $ execucao           <fct> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
## $ data_hora          <dttm> 2026-10-31 12:31:54, 2026-10-31 12:31:59, 2026-10-…
## $ horas              <dbl> 0.00638, 0.00776, 0.00916, 0.01055, 0.01193, 0.0133…
## $ fps                <dbl> 16.60, 25.72, 30.11, 26.57, 26.50, 25.68, 25.52, 25…
## $ frametime_p99_ms   <dbl> 730.127, 72.052, 58.570, 61.369, 55.475, 46.835, 46…
## $ gpu_time_ms        <dbl> 30.638, 18.863, 20.551, 22.952, 27.826, 23.015, 26.…
## $ game_thread_ms     <dbl> 4.569, 4.321, 5.957, 4.978, 9.440, 6.873, 3.436, 2.…
## $ render_thread_ms   <dbl> 33.484, 25.842, 27.935, 31.238, 32.530, 29.189, 23.…
## $ proc_mem_mb        <dbl> 2188.1, 2225.2, 2293.0, 2223.4, 2310.5, 2235.1, 223…
## $ proc_private_mb    <dbl> 1335.5, 1348.6, 1352.5, 1362.9, 1365.9, 1370.5, 137…
## $ proc_threads       <int> 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70,…
## $ proc_handles       <int> 365, 365, 365, 365, 365, 365, 365, 365, 365, 365, 3…
## $ proc_cpu_pct       <dbl> NA, 50.3, 51.5, 48.9, 50.4, 50.9, 48.0, 44.7, 44.4,…
## $ cpu_total_pct      <dbl> 0.0, 52.9, 54.1, 51.3, 52.5, 53.1, 49.6, 46.2, 46.4…
## $ cpu_nucleo_max_pct <dbl> 0.0, 53.6, 57.1, 54.0, 53.6, 55.4, 52.3, 48.2, 49.5…
## $ outros_cpu_pct     <dbl> NA, 2.7, 2.5, 2.4, 2.1, 2.2, 1.6, 1.5, 2.0, 2.3, 1.…
## $ ram_mb             <dbl> 4138.8, 4111.5, 4078.1, 4043.2, 4064.4, 4032.0, 401…
## $ vram_mb            <dbl> 3912.0, 4045.9, 4155.2, 4123.1, 4235.2, 4235.1, 429…
## $ gpu_util_pct       <dbl> 71, 57, 60, 67, 67, 71, 80, 66, 75, 75, 74, 73, 65,…
## $ gpu_temp_c         <dbl> 67, 67, 67, 71, 74, 74, 76, 77, 78, 78, 80, 80, 77,…
## $ gpu_clock_mhz      <dbl> 1905, 1920, 1920, 1920, 1905, 1905, 1890, 1905, 186…
## $ gpu_power_w        <dbl> 231.1, 192.0, 190.5, 208.2, 222.4, 218.1, 238.6, 23…
## $ throttle_power     <lgl> TRUE, FALSE, FALSE, TRUE, TRUE, TRUE, TRUE, TRUE, T…
## $ throttle_thermal   <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FA…

Execuções do jogo

metricas |>
  group_by(plataforma, execucao) |>
  summarise(amostras = n(),
            inicio   = min(data_hora),
            fim      = max(data_hora),
            .groups  = "drop") |>
  kable(caption = "Amostras por plataforma e execução") |>
  kable_styling(full_width = FALSE)
Amostras por plataforma e execução
plataforma execucao amostras inicio fim
Linux + Proton 1 5759 2026-10-31 12:31:54 2026-10-31 20:31:44
Windows 1 31 2026-11-02 14:50:27 2026-11-02 14:52:54
Windows 2 5725 2026-11-02 14:53:19 2026-11-02 22:50:19

Período de aquecimento

Nos primeiros minutos de cada execução, a cena ainda está sendo carregada na memória de vídeo e a GPU ainda está esquentando, então os valores não representam o funcionamento normal. Para identificar quanto tempo dura esse período, calculamos os minutos desde o início de cada execução e observamos os primeiros 30 minutos.

metricas <- metricas |>
  group_by(plataforma, execucao) |>                       # cada execução tem seu próprio início
  mutate(min_execucao = (horas - min(horas)) * 60) |>     # horas -> minutos desde o início
  ungroup()

aquecimento_min <- 5   # duração do aquecimento, definida a partir do gráfico abaixo
metricas |>
  filter(min_execucao < 30) |>                                    # só os primeiros 30 min
  mutate(minuto = floor(min_execucao)) |>                         # agrupa as amostras por minuto
  group_by(plataforma, execucao, minuto) |>
  summarise(across(c(fps, vram_mb, gpu_temp_c), mean),            # média de cada minuto
            .groups = "drop") |>
  pivot_longer(c(fps, vram_mb, gpu_temp_c),
               names_to = "metrica", values_to = "valor") |>      # largo -> longo para o facet
  mutate(metrica = recode(metrica,
                          fps        = "FPS",
                          vram_mb    = "VRAM (MB)",
                          gpu_temp_c = "Temperatura da GPU (°C)")) |>
  ggplot(aes(x = minuto, y = valor, color = plataforma, linetype = execucao)) +
  annotate("rect", xmin = 0, xmax = aquecimento_min, ymin = -Inf, ymax = Inf,
           fill = "grey85", alpha = 0.6) +                        # faixa do aquecimento
  geom_line(linewidth = 0.8) +
  facet_wrap(~ metrica, ncol = 1, scales = "free_y") +            # um painel por métrica
  labs(title    = "Os primeiros minutos de cada execução",
       subtitle = "Média por minuto. Faixa cinza: período descartado como aquecimento",
       x = "Minutos desde o início da execução", y = NULL,
       color = "Plataforma", linetype = "Execução") +
  theme_minimal()

A VRAM e a temperatura sobem rapidamente nos primeiros 2 a 3 minutos e depois se estabilizam nas duas plataformas. Por segurança, descartamos os primeiros 5 minutos de cada execução. Essa regra resolve outros dois problemas de uma vez: remove as amostras iniciais com uso de CPU ausente ou zerado e remove a primeira execução do Windows, que durou só 2,5 minutos antes de o jogo fechar.

antes <- count(metricas, plataforma, name = "antes")    # amostras antes do filtro

metricas <- metricas |>
  filter(min_execucao >= aquecimento_min) |>            # remove o aquecimento
  droplevels()                                          # remove níveis de fator que ficaram vazios

antes |>
  left_join(count(metricas, plataforma, name = "depois"), by = "plataforma") |>
  mutate(removidas = antes - depois) |>
  kable(caption = "Amostras removidas pelo filtro de aquecimento") |>
  kable_styling(full_width = FALSE)
Amostras removidas pelo filtro de aquecimento
plataforma antes depois removidas
Linux + Proton 5759 5698 61
Windows 5756 5664 92

Limpeza da base de processos

A base de processos registra, a cada minuto, os processos do sistema que mais consumiam recursos durante o teste. Ela passa por quatro ajustes:

  1. Junção e fuso horário, como na base de métricas.
  2. Padronização dos nomes: no Windows, os nomes terminam em .exe e misturam maiúsculas e minúsculas (MsMpEng.exe, System); no Linux, não (gnome-shell). Removemos a extensão e colocamos tudo em minúsculas.
  3. Remoção do próprio jogo: no Linux, o jogo aparece como GameThread, porque sob o Proton o processo recebe o nome da thread principal da Unreal. Como o jogo já está medido na base de métricas, ele é removido aqui, para que esta base contenha apenas os outros processos.
  4. Remoção do aquecimento: mantemos apenas as leituras a partir do mesmo instante em que começa a base de métricas limpa de cada plataforma.
processos <- bind_rows(
  "Linux + Proton" = processos_linux,
  "Windows"        = processos_windows,
  .id = "plataforma"
) |>
  mutate(
    plataforma = factor(plataforma),
    data_hora  = with_tz(timestamp_utc, tzone = "America/Recife"),
    processo   = processo |>
      str_remove("\\.exe$") |>          # tira o ".exe" do final do nome
      str_to_lower(),                    # padroniza tudo em minúsculas
    pid        = as.integer(pid)         # identificador é número inteiro
  ) |>
  filter(processo != "gamethread") |>    # remove o próprio jogo (Linux)
  select(plataforma, data_hora, horas, processo, pid, cpu_pct, mem_mb)

# Instante em que a base de métricas limpa começa, para cada plataforma
inicio_valido <- metricas |>
  group_by(plataforma) |>
  summarise(inicio = min(data_hora))

processos <- processos |>
  left_join(inicio_valido, by = "plataforma") |>   # cada linha ganha o "inicio" da sua plataforma
  filter(data_hora >= inicio) |>                    # descarta o aquecimento
  select(-inicio)                                   # a coluna auxiliar não é mais necessária
processos |>
  group_by(plataforma, processo) |>
  summarise(leituras     = n(),
            cpu_medio    = round(mean(cpu_pct), 3),
            mem_media_mb = round(mean(mem_mb), 1),
            .groups = "drop") |>
  kable(caption = "Outros processos ativos durante o teste",
        col.names = c("Plataforma", "Processo", "Leituras",
                      "CPU média (%)", "Memória média (MB)")) |>
  kable_styling(full_width = FALSE)
Outros processos ativos durante o teste
Plataforma Processo Leituras CPU média (%) Memória média (MB)
Linux + Proton gnome-shell 474 0.000 432.3
Linux + Proton gnome-software 474 0.008 519.3
Windows aggregatorhost 1 1.400 9.1
Windows msmpeng 470 0.026 345.8
Windows nvcontainer 2 2.050 73.4
Windows system 1 2.600 1.6