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:
- 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.
- Comparação: confrontar as plataformas não só pela média, mas pela estabilidade (variabilidade e travadas) e pela eficiência (FPS por watt).
- 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)| 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.
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)| 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)| 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 abaixometricas |>
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)| 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:
- Junção e fuso horário, como na base de métricas.
- Padronização dos nomes: no Windows, os nomes
terminam em
.exee 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. - 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. - 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áriaprocessos |>
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)| 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 |