Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Before publishing a version, add a `## [VERSION]` section to **both** files: the workflow publishes the section matching the tag from each one and fails if either is missing.

## [Unreleased]
### What's new
- Revamped FPS counter: the numbers sit on a dark rounded card (it can be turned off) in three sizes, Small, Medium and Large. You can pick the screen corner it shows in (or keep RTSS's position) and tune it all from the session menu itself: A on "FPS counter" opens the list with style, size, position, background, color and, in Custom, the items. New styles: FPS only, Compact (FPS plus CPU, GPU and RAM usage on one line) and Detailed (FPS, frame time and API, plus CPU clock, GPU clock and temperature, VRAM and RAM). In Custom, the PC items can be picked one by one too. The data comes from Windows and the graphics driver, nothing to install; anyone already on "Compact" keeps seeing just the FPS. (#146)

### Changes
- The session menu no longer shows the "Record the last 30 s · coming soon" row, which never worked. (#147)

Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CHANGELOG.en-US.md`. Antes de publicar uma versão, adicione uma seção `## [VERSÃO]` com o changelog daquela versão **nos dois arquivos**. O workflow publica as duas seções correspondentes à tag na mesma release e falha se faltar alguma.

## [Unreleased]
### Novidades
- Contador de FPS repaginado: os números ficam num card escuro e arredondado (dá para desligar) em três tamanhos, Pequeno, Médio e Grande. Dá para escolher o canto da tela onde ele aparece (ou deixar a posição do RTSS) e ajustar tudo pelo próprio menu da sessão: A em "Contador de FPS" abre a lista com estilo, tamanho, posição, fundo, cor e, no Personalizado, os itens. Novos estilos: Apenas FPS, Compacto (FPS e uso de CPU, GPU e RAM numa linha) e Detalhado (FPS, tempo de quadro e API, mais clock da CPU, clock e temperatura da GPU, VRAM e RAM). No Personalizado, os itens do PC também podem ser escolhidos um a um. Os dados vêm do Windows e do driver de vídeo, sem instalar nada; quem já usava "Compacto" continua vendo só o FPS. (#146)

### Mudanças
- O menu da sessão não mostra mais a linha "Gravar os últimos 30 s · em breve", que nunca funcionou. (#147)

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ You pick the controller shortcuts yourself: none is on at first, the app asks th

- Hide the other screens by **disconnecting** them, **black overlays** or **DDC/CI**, all with Windows' own APIs: no third-party display or audio tool is bundled
- Per-screen **resolution and refresh rate**, **HDR**, **VRR** and an **FPS limit** (RTSS) while you play
- **FPS counter** over the game (RTSS): compact, detailed or your own layout
- **FPS counter** over the game (RTSS) on a card in three sizes: FPS only, compact (CPU, GPU and RAM usage), detailed (clocks, GPU temperature and memory) or your own layout
- Audio goes to the TV (or any output) and comes back afterwards
- [Turns the TV on and switches it to the PC's input](docs/GUIDE.md#tv-control) (Google TV / Android TV over the network), and can put it in standby afterwards
- Launches **Steam Big Picture**, **Playnite fullscreen** or **Xbox** mode, and sends Steam to the tray when you go back to the PC
Expand Down
2 changes: 1 addition & 1 deletion README.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Os atalhos do controle são você quem escolhe: nenhum vem ligado, o app pergunt

- Esconde as outras telas **desconectando**, com **cortinas pretas** ou por **DDC/CI**, tudo pelas APIs do próprio Windows: nenhuma ferramenta de terceiros para telas ou áudio vem embutida
- **Resolução e Hz** por tela, **HDR**, **VRR** e **limite de FPS** (RTSS) enquanto você joga
- **Contador de FPS** sobre o jogo (RTSS): compacto, detalhado ou do seu jeito
- **Contador de FPS** sobre o jogo (RTSS) num card em três tamanhos: apenas FPS, compacto (uso de CPU, GPU e RAM), detalhado (clocks, temperatura da GPU e memória) ou do seu jeito
- O áudio vai para a TV (ou qualquer saída) e volta depois
- [Liga a TV e troca para a entrada do PC](docs/GUIDE.pt-BR.md#controle-da-tv) (Google TV / Android TV pela rede), e pode colocá-la em espera no fim
- Abre **Steam Big Picture**, **Playnite em tela cheia** ou o **Modo Xbox**, e manda a Steam para a bandeja ao voltar ao PC
Expand Down
21 changes: 20 additions & 1 deletion docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,8 @@ Pré-requisito: RivaTuner Statistics Server instalado, com "Show On-Screen Displ
- [ ] Sem o RTSS instalado, a linha "Contador de FPS" não aparece no menu da sessão e o cartão em Ajustes fica desabilitado.
- [ ] Na prévia (menu aberto fora de uma sessão), a linha não aparece.
- [ ] Config nova: o estilo é "Afterburner (não mexer)" e o OSD do Afterburner aparece normal na sessão.
- [ ] Em sessão, A na linha alterna Afterburner → Desligado → Compacto → Detalhado → Personalizado → Afterburner, e o jogo muda na hora.
- [ ] Em sessão, A na linha "Contador de FPS" abre a lista de ajustes do contador. A em "Estilo" alterna Afterburner → Desligado → Apenas FPS → Compacto → Detalhado → Personalizado → Afterburner, e o jogo muda na hora; a lista continua aberta com o foco na mesma linha.
- [ ] Na lista do contador, Tamanho, Posição, Fundo em card e Cor mudam o contador na hora; com Personalizado aparecem "Tudo em uma linha" e os itens (com ✓ nos ligados), e A liga/desliga cada um. B fecha a lista e volta para a linha do contador. Os ajustes ficam salvos e aparecem iguais em Ajustes.
- [ ] Desligado: nada na tela, nem o nosso nem o do Afterburner (no máximo um piscar rápido do Afterburner de vez em quando).
- [ ] Compacto / Detalhado / Personalizado: só o nosso aparece; o do Afterburner fica escondido.
- [ ] Afterburner: o nosso some e o do Afterburner volta em até 1 s.
Expand All @@ -395,3 +396,21 @@ Pré-requisito: RivaTuner Statistics Server instalado, com "Show On-Screen Displ
- [ ] Voltar ao PC: o nosso some e o do Afterburner volta.
- [ ] A escolha e o layout ficam salvos para a próxima sessão.
- [ ] Com Compacto (ou outro estilo nosso) já salvo em Ajustes, iniciar a sessão: o contador aparece e o do Afterburner some sem precisar mexer na linha do menu.

### Card, tamanhos e dados do PC (1.6.1)

- [ ] Apenas FPS / Compacto / Detalhado aparecem dentro de um card escuro, translúcido e de cantos arredondados, com uma folga em volta do texto; o card acompanha o tamanho do texto (não cobre a tela toda nem o OSD de outro programa).
- [ ] Posição "A do RTSS": o card fica na posição do OSD escolhida no RTSS.
- [ ] Posição em cada um dos quatro cantos: o contador vai para aquele canto da tela, com e sem o card, e não sai da tela nos cantos da direita e de baixo.
- [ ] "Fundo em card" desligado: o texto volta a ficar direto sobre o jogo, sem fundo.
- [ ] Tamanho Pequeno / Médio / Grande muda o tamanho de todo o contador na hora; no Grande o texto continua nítido e as unidades (FPS, %, GHz) ficam alinhadas na base dos números.
- [ ] Compacto: uma linha com FPS, CPU %, GPU % e RAM %; os números não fazem o texto "pular" quando mudam de 2 para 3 dígitos.
- [ ] Detalhado: FPS, tempo de quadro e API na primeira linha; CPU (uso e clock), GPU (uso, clock e °C), VRAM (usada / total) e RAM (usada / total) em linhas alinhadas.
- [ ] Os valores batem com o Gerenciador de Tarefas (CPU %, velocidade, GPU %, memória dedicada, temperatura da GPU) e atualizam a cada ~1 s.
- [ ] No primeiro segundo os valores aparecem como "--" e depois são preenchidos.
- [ ] Notebook com GPU integrada + dedicada: os dados de GPU são da placa que está rodando o jogo.
- [ ] GPU cujo driver não informa temperatura ou clock: esses itens simplesmente não aparecem (sem "0 MHz").
- [ ] Personalizado: marcar/desmarcar cada item do PC (uso/clock da CPU, RAM, VRAM, uso/clock/temperatura da GPU) muda o contador na hora; com "Tudo em uma linha" ligado, fica tudo numa linha.
- [ ] Config salva na 1.6.0 com "Compacto": depois de atualizar, aparece "Apenas FPS" e o jogo mostra só o FPS.
- [ ] Voltar ao PC com Compacto/Detalhado: o contador some e não volta sozinho segundos depois.
- [ ] Uso de CPU do Console Mode com Detalhado ligado continua baixo (Gerenciador de Tarefas, < 1%).
2 changes: 1 addition & 1 deletion src/ConsoleMode/Models/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public sealed class AppConfig
public string AudioDeviceName { get; set; } = "";
public bool AudioAutoSwitch { get; set; }
public int FpsLimit { get; set; }
/// <summary>RtssOverlay style: "external" | "off" | "compact" | "detailed" | "custom". Applied over the game during a session.</summary>
/// <summary>RtssOverlay style: "external" | "off" | "compact" (FPS only) | "stats" | "detailed" | "custom". Applied over the game during a session.</summary>
public string FpsOverlay { get; set; } = RtssOverlay.External;
public FpsOverlayLayout FpsOverlayLayout { get; set; } = new();
public Dictionary<string, SavedDisplayMode> MonitorModes { get; set; } = new(StringComparer.OrdinalIgnoreCase);
Expand Down
128 changes: 128 additions & 0 deletions src/ConsoleMode/Native/GpuKmt.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System.Runtime.InteropServices;

namespace ConsoleMode.Native;

/// <summary>
/// GPU temperature, engine clock and VRAM size straight from the display driver (D3DKMT, WDDM 2.4+),
/// the source of Task Manager's GPU temperature: no vendor library, driver or admin needed.
/// Drivers that don't report a value leave it 0, read here as null.
/// </summary>
public static class GpuKmt
{
private const int KmtqaiGetSegmentSize = 3;
private const int KmtqaiNodePerfData = 61;
private const int KmtqaiAdapterPerfData = 62;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These performance-query enum values are each one too high: NODEPERFDATA is 60 and ADAPTERPERFDATA is 61 in the Windows enum, so these calls request the wrong payload types and the GPU clock/temperature reads will fail. Please use the documented values (or named constants) before relying on these readings.

Technical details
# D3DKMT query type mismatch

## Affected sites
- `src/ConsoleMode/Native/GpuKmt.cs:13-14` — enum values 61/62 select `ADAPTERPERFDATA` / `ADAPTERPERFDATA_CAPS`, rather than `NODEPERFDATA` / `ADAPTERPERFDATA`.

## Required outcome
- Match the `KMTQUERYADAPTERINFOTYPE` values for node and adapter performance data (60 and 61).

## Reference
- https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/d3dkmthk/ne-d3dkmthk-_kmtqueryadapterinfotype


[StructLayout(LayoutKind.Sequential)]
private struct OpenAdapterFromLuid
{
public uint LuidLow;
public int LuidHigh;
public uint Adapter;
}

[StructLayout(LayoutKind.Sequential)]
private struct CloseAdapter
{
public uint Adapter;
}

[StructLayout(LayoutKind.Sequential)]
private struct QueryAdapterInfo
{
public uint Adapter;
public int Type;
public nint Data;
public uint DataSize;
}

// d3dkmthk.h: D3DKMT_SEGMENTSIZEINFO, D3DKMT_NODE_PERFDATA, D3DKMT_ADAPTER_PERFDATA (ULONGLONGs are 8-aligned).
[StructLayout(LayoutKind.Sequential)]
private struct SegmentSizeInfo
{
public ulong DedicatedVideoMemorySize;
public ulong DedicatedSystemMemorySize;
public ulong SharedSystemMemorySize;
}

[StructLayout(LayoutKind.Sequential)]
private struct NodePerfData
{
public uint NodeOrdinal;
public uint PhysicalAdapterIndex;
public ulong Frequency;
public ulong MaxFrequency;
public ulong MaxFrequencyOc;
public uint Voltage;
public uint VoltageMax;
public uint VoltageMaxOc;
public ulong MaxTransitionLatency;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The native D3DKMT_NODE_PERFDATA also has a trailing aligned 64-bit Reserved field. Since Query sends sizeof(T) as DataSize, this shorter buffer can make the node query fail even after the enum is corrected; mirror the full native layout.

Technical details
# Node performance payload size

## Affected sites
- `src/ConsoleMode/Native/GpuKmt.cs:49-60` — `NodePerfData` omits `Reserved`.
- `src/ConsoleMode/Native/GpuKmt.cs:124` — `DataSize` is derived from the undersized struct.

## Required outcome
- Submit a `NodePerfData` buffer with the documented native size and field layout.

## Reference
- https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/d3dkmthk/ns-d3dkmthk-_d3dkmt_node_perfdata

}

[StructLayout(LayoutKind.Sequential)]
private struct AdapterPerfData
{
public uint PhysicalAdapterIndex;
public ulong MemoryFrequency;
public ulong MaxMemoryFrequency;
public ulong MaxMemoryFrequencyOc;
public ulong MemoryBandwidth;
public ulong PcieBandwidth;
public uint FanRpm;
public uint Power;
public uint Temperature;
public byte PowerStateOverride;
}

[DllImport("gdi32.dll")]
private static extern int D3DKMTOpenAdapterFromLuid(ref OpenAdapterFromLuid open);

[DllImport("gdi32.dll")]
private static extern int D3DKMTCloseAdapter(ref CloseAdapter close);

[DllImport("gdi32.dll")]
private static extern int D3DKMTQueryAdapterInfo(ref QueryAdapterInfo query);

public readonly record struct Reading(double? TemperatureC, double? ClockMhz, double? DedicatedMb);

/// <summary>One read of the adapter (LUID as in the GPU performance counters) and its engine <paramref name="node"/>.</summary>
public static Reading Read(uint luidLow, int luidHigh, uint node)
{
var open = new OpenAdapterFromLuid { LuidLow = luidLow, LuidHigh = luidHigh };
try
{
if (D3DKMTOpenAdapterFromLuid(ref open) != 0) return default;
}
catch (EntryPointNotFoundException)
{
return default;
}
try
{
var adapter = new AdapterPerfData();
var engine = new NodePerfData { NodeOrdinal = node };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PhysicalAdapterIndex is an input to both performance queries, but both structs currently leave it at zero. The PDH instance already identifies phys_N; propagate that index so GPU clock and temperature queries target the selected physical adapter on linked-adapter systems.

Technical details
# Physical adapter selection

## Affected sites
- `src/ConsoleMode/Services/HardwareStats.cs:27, 60-61` — the selected adapter key includes the `phys_N` component, but the index is not exposed to the KMT reader.
- `src/ConsoleMode/Native/GpuKmt.cs:65, 103` — the performance payloads leave `PhysicalAdapterIndex` at zero.

## Required outcome
- Pass the selected physical adapter index to both adapter- and node-performance queries.

## Reference
- https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/d3dkmthk/ns-d3dkmthk-_d3dkmt_node_perfdata
- https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/d3dkmthk/ns-d3dkmthk-_d3dkmt_adapter_perfdata

var segments = new SegmentSizeInfo();
double? temperature = Query(open.Adapter, KmtqaiAdapterPerfData, ref adapter) && adapter.Temperature > 0
? adapter.Temperature / 10.0 : null;
double? clock = Query(open.Adapter, KmtqaiNodePerfData, ref engine) && engine.Frequency > 0
? engine.Frequency / 1_000_000.0 : null;
double? dedicated = Query(open.Adapter, KmtqaiGetSegmentSize, ref segments) && segments.DedicatedVideoMemorySize > 0
? segments.DedicatedVideoMemorySize / (1024.0 * 1024.0) : null;
return new Reading(temperature, clock, dedicated);
}
finally
{
var close = new CloseAdapter { Adapter = open.Adapter };
D3DKMTCloseAdapter(ref close);
}
}

private static unsafe bool Query<T>(uint adapter, int type, ref T data) where T : unmanaged
{
fixed (T* p = &data)
{
var query = new QueryAdapterInfo { Adapter = adapter, Type = type, Data = (nint)p, DataSize = (uint)sizeof(T) };
return D3DKMTQueryAdapterInfo(ref query) == 0;
}
}
}
105 changes: 105 additions & 0 deletions src/ConsoleMode/Native/PerfCounters.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System.Runtime.InteropServices;

namespace ConsoleMode.Native;

/// <summary>
/// Windows performance counters (PDH), the same source Task Manager reads. Counter paths are the
/// English names (PdhAddEnglishCounter), so they work on a Windows in any language.
/// </summary>
public sealed class PerfCounters : IDisposable
{
private const uint PdhFmtDouble = 0x00000200;
private const uint PdhFmtNoCap100 = 0x00008000;
private const int PdhMoreData = unchecked((int)0x800007D2);

[DllImport("pdh.dll", CharSet = CharSet.Unicode)]
private static extern int PdhOpenQueryW(string? dataSource, nint userData, out nint query);

[DllImport("pdh.dll", CharSet = CharSet.Unicode)]
private static extern int PdhAddEnglishCounterW(nint query, string path, nint userData, out nint counter);

[DllImport("pdh.dll")]
private static extern int PdhCollectQueryData(nint query);

[DllImport("pdh.dll")]
private static extern int PdhGetFormattedCounterValue(nint counter, uint format, out uint type, out FmtCounterValue value);

[DllImport("pdh.dll", CharSet = CharSet.Unicode)]
private static extern int PdhGetFormattedCounterArrayW(nint counter, uint format, ref uint bufferSize, out uint itemCount, nint buffer);

[DllImport("pdh.dll")]
private static extern int PdhCloseQuery(nint query);

[StructLayout(LayoutKind.Sequential)]
private struct FmtCounterValue
{
public uint Status;
public double Value;
}

[StructLayout(LayoutKind.Sequential)]
private struct FmtCounterValueItem
{
public nint Name;
public FmtCounterValue Value;
}

private nint _query;

public PerfCounters()
{
if (PdhOpenQueryW(null, 0, out _query) != 0) _query = 0;
}

/// <summary>0 when the counter doesn't exist on this machine (old Windows, no GPU driver…).</summary>
public nint Add(string path) =>
_query != 0 && PdhAddEnglishCounterW(_query, path, 0, out var counter) == 0 ? counter : 0;

/// <summary>Takes a sample; rates (usage %) need two, so the first read after this returns null.</summary>
public void Collect()
{
if (_query != 0) PdhCollectQueryData(_query);
}

public double? Read(nint counter)
{
if (counter == 0) return null;
return PdhGetFormattedCounterValue(counter, PdhFmtDouble | PdhFmtNoCap100, out _, out var value) == 0 && value.Status == 0
? value.Value : null;
}

/// <summary>Every instance of a wildcard counter (…(*)\…) with its value.</summary>
public List<(string Instance, double Value)> ReadAll(nint counter)
{
var result = new List<(string, double)>();
if (counter == 0) return result;
uint size = 0;
if (PdhGetFormattedCounterArrayW(counter, PdhFmtDouble | PdhFmtNoCap100, ref size, out _, 0) != PdhMoreData || size == 0)
return result;
var buffer = Marshal.AllocHGlobal((int)size);
try
{
if (PdhGetFormattedCounterArrayW(counter, PdhFmtDouble | PdhFmtNoCap100, ref size, out var count, buffer) != 0)
return result;
var itemSize = Marshal.SizeOf<FmtCounterValueItem>();
for (var i = 0; i < count; i++)
{
var item = Marshal.PtrToStructure<FmtCounterValueItem>(buffer + i * itemSize);
if (item.Value.Status == 0 && Marshal.PtrToStringUni(item.Name) is { } name)
result.Add((name, item.Value.Value));
}
}
finally
{
Marshal.FreeHGlobal(buffer);
}
return result;
}

public void Dispose()
{
if (_query == 0) return;
PdhCloseQuery(_query);
_query = 0;
}
}
32 changes: 32 additions & 0 deletions src/ConsoleMode/Native/SystemMemory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Runtime.InteropServices;

namespace ConsoleMode.Native;

public static class SystemMemory
{
[StructLayout(LayoutKind.Sequential)]
private struct MemoryStatusEx
{
public uint Length;
public uint MemoryLoad;
public ulong TotalPhys;
public ulong AvailPhys;
public ulong TotalPageFile;
public ulong AvailPageFile;
public ulong TotalVirtual;
public ulong AvailVirtual;
public ulong AvailExtendedVirtual;
}

[DllImport("kernel32.dll")]
private static extern bool GlobalMemoryStatusEx(ref MemoryStatusEx status);

/// <summary>RAM in use and installed, in MB; nulls if Windows doesn't answer.</summary>
public static (double? UsedMb, double? TotalMb) Read()
{
var status = new MemoryStatusEx { Length = (uint)Marshal.SizeOf<MemoryStatusEx>() };
if (!GlobalMemoryStatusEx(ref status) || status.TotalPhys == 0) return (null, null);
const double mb = 1024 * 1024;
return ((status.TotalPhys - status.AvailPhys) / mb, status.TotalPhys / mb);
}
}
Loading
Loading