Client
Interact with Intel® product support specialists on client concerns and recommendations
52 Discussions

How GOALS Delivers Sustained, Competitive Esports Performance on Handheld PCs (Part 1)

Thomas_Hannaford
Employee
0 0 455

Playing to the Strengths of Intel® Core™ Ultra Processors: How GOALS Delivers Sustained, Competitive Esports Performance on Handheld PCs — Part 1: Power Management and Thermal Regulation 

By Torbjörn Söderman, Technical Director, GOALS 

 

When people think about competitive esports, they typically picture high-refresh-rate monitors, discrete GPUs, and desktop rigs with unconstrained power budgets. The reality of who is actually playing GOALS is more varied and more interesting. 

GOALS is a free-to-play competitive football game, using our custom-built, patent-pending SENTEC simulation engine with Unreal Engine for the presentation layer, designed for crossplay across PC, console, mobile, and handheld. During our playtests in alpha, hardware telemetry revealed something instructive: a meaningful share of our players were on hardware that many would consider obsolete, graphics cards dating back to 2013, and up to 9% of the player population on devices that fell into this category. Experience from other free-to-play titles suggests that number could climb toward 30% post-launch. 

At the same time, players on the opposite end of the spectrum, those with high-end discrete GPUs and modern Intel® Core™ Ultra Processors for Handheld PC Gaming, were reporting a different problem entirely: GPU fan noise, coil whine, and in some cases, hardware thermal shutdowns. The game was simply running at full throttle when it didn't need to. 

Both problems have the same root cause: a lack of intentional power management. And both were addressed through the same architecture. 

This article covers how Intel hardware and APIs, specifically the Intel® Graphics Control Library (IGCL) and the Xe GPU power model, enabled us to build a power-aware performance system that scales from decade-old integrated graphics to current Intel discrete GPUs with everything between including Intel Xe-powered handhelds like the Claw.  

 

The Problem With Unconstrained Frame Rates 

A competitive esports game with a well-optimised renderer can generate frame rates far beyond what a monitor can display or a player can perceive. On a 144 Hz display, rendering at 400 FPS wastes significant GPU work while generating substantial heat, fan noise, and coil whine, an audible hum from electrical components vibrating under high current draw. 

For players in hot climates, this is not a minor annoyance. PC fans running at maximum RPM in an already warm room can meaningfully degrade the play experience. A player who associates your game with an uncomfortably hot room will eventually stop launching it. 

The traditional answer to this is VSync. But VSync has well-known problems in a competitive context: it introduces input latency, and on monitors with very high refresh rates it doesn't meaningfully cap performance at a comfortable level. A player reporting 153 FPS before enabling VSync dropped to 60 FPS afterward, an unacceptable regression for a player who reasonably expects to sustain 120+ FPS on their hardware. 

What we needed was something more surgical: a system that could target a specific thermal and acoustic state, rather than a fixed frame number. 

 

Two Builds Across the Full Hardware Spectrum 

Before getting into the power management system, it helps to understand how GOALS approaches the breadth of PC hardware in the first place. 

Thomas_Hannaford_0-1781552503473.png

 

We ship two PC variants. 

The Experience build targets mainstream and modern hardware, including all current Intel Arc-powered handhelds. It runs the full rendering pipeline with deferred shading, full material complexity, and the complete feature set. On WinGDK (the build distributed through the Microsoft Store and Xbox App), this is the only build we ship. Game Pass players always receive the Experience build, with runtime scaling and device profiles handling the adaptation to different handheld hardware. 

The Performance build targets legacy integrated GPUs and early-generation low-end hardware. For the most extreme cases at the very bottom of the hardware range, we took a lesson from mobile development and enabled Unreal's ES3.1 mobile rendering path on desktop. The result was 4–6ms faster frames and the ability to push genuinely "potato" devices to a playable 30+ FPS. 

Shipping as a separate binary pays dividends beyond the rendering path switch. Because it is a distinct build, we can include cheaper, lower-memory versions of assets without those compromises touching the Experience build at all, reducing both memory pressure and streaming cost on machines that may have as little as 4GB of RAM. The engineering constraints of supporting very old hardware stay cleanly contained, rather than leaking into the Experience build as a lowest-common-denominator tax. 

An important benefit of this separation: optimisations at the low end often propagate upward. Better frame timing, reduced draw call pressure, and tighter CPU usage patterns improve the high-end experience as well. 

For Intel Arc-powered handhelds, the Experience build is the right home. But delivering a good experience on these devices requires understanding what makes them architecturally different from discrete GPU setups. 

 

GPUThrottle: A Power-Aware Frame Regulation System 

The core of our power management approach is an Unreal engine subsystem called UGoalsGPUThrottlingSubsystem. It reads real-time GPU telemetry through hardware vendor APIs and uses it to regulate the frame rate dynamically via Unreal's SetMaxFPS(), rather than imposing a static cap. 

The subsystem is controlled through console variables, making it fully scriptable through Unreal's scalability system and easily exposed to the player-facing settings menu. Four operating modes are available: 

Off — no throttling. The GPU works as hard and fast as possible. High heat and loud fan noise are the expected result on capable hardware. 

Fixed FPS (goals.GPUThrottleMethod 1) — sets a specific maximum FPS target via goals.GPUFixedFPSThreshold. A more granular alternative to VSync that avoids VSync's latency and refresh-rate coupling problems. The most familiar mode for players. 

Silent (goals.GPUThrottleMethod 2) — targets both a GPU temperature and a fan RPM threshold simultaneously, weighted as a composite control signal. The goal is not a specific frame rate, it is the highest sustainable frame rate that keeps the device below both a thermal and an acoustic limit. 

Temperature (goals.GPUThrottleMethod 3) — dynamically adjusts the FPS cap to target a specific GPU temperature (goals.GPUTempThreshold, default 70°C). Frame rate floats up when thermal headroom exists and pulls back as the device approaches its ceiling. 

The subsystem also manages state transitions between InGame and MainMenu. In MainMenu, the frame rate is hard-capped to LowerFPSValue (30 on low-end hardware, 60 otherwise). Beyond that, the subsystem implements idle detection: after 45 seconds of no player input, detected via FSlateApplication::Get().GetLastUserInteractionTime(), the menu drops further to 15 FPS. Any input within a 0.2-second window immediately cancels the idle state and restores the normal menu cap. When the player transitions out of the menu, idle state is also cleared automatically. Both thresholds are configurable via console variables (goals.MenuIdleThresholdSeconds and goals.MenuIdleFPS). 

Menus have no competitive sensitivity and no reason to drive the GPU to full load. A player who opens the game, navigates to the settings screen, and then walks away to make coffee should not be running their GPU at full power in the background. 

 

Implementing IGCL: Threading, Telemetry, and Capability Detection 

The most important architectural decision in the IGCL integration is where driver calls happen. Calling GPU telemetry APIs on the game thread every frame is a reliable way to introduce frame time variance — vendor driver code does not make timing guarantees. Our solution is to push all IGCL calls onto a dedicated background thread. 

The base class GoalsGPUThrottle inherits from Unreal's FRunnable and owns a dedicated FRunnableThread. Its Run() loop calls the pure virtual UpdateTelemetry() at a fixed 1-second interval and stores results in std::atomic<float> members: 

c 

virtual uint32 Run() override 

{ 

    while (bRunning) 

    { 

        UpdateTelemetry(); 

        FPlatformProcess::Sleep(1.0f); 

    } 

    return 0; 

} 

GoalsINTELThrottle implements UpdateTelemetry() to call IGCL and write the results into the atomics: 

c 

void GoalsINTELThrottle::UpdateTelemetry() 

{ 

    CachedTemperature.store(GetIGCLGPUTemperature(),       std::memory_order_relaxed); 

    CachedPowerUsage.store(GetIGCLPowerUsage(),            std::memory_order_relaxed); 

    CachedFanSpeed.store(GetIntelGPUFanPercentage(),       std::memory_order_relaxed); 

    CachedMemoryTemperature.store(GetIGCLGPUMemoryTemperature(), std::memory_order_relaxed); 

    // ... frequency domain / throttle reason polling 

} 

The game thread reads these atomics with no locks, no driver calls, and no frame time impact. The worst-case staleness of the telemetry data is one second, which is entirely acceptable for a thermal regulation loop whose own update cadence is measured in seconds. 

This pattern, background thread owns driver calls, game thread reads atomics, is the right general approach for any vendor GPU telemetry API integrated into a game loop. 

Initialisation follows a deliberate two-pass sequence. ctlInit() is called with CTL_INIT_FLAG_USE_LEVEL_ZERO, then ctlEnumerateDevices() is called twice — once to retrieve the adapter count, once to fill the handle array — and the first adapter is selected. If no Intel device is found or initialisation fails at any step, GoalsINTELThrottle cleans up and the subsystem falls back to the vendor-agnostic path with no IGCL calls, no throttling, and standard PC defaults. 

Temperature sensors are enumerated and separated by type. The GPU core sensor (CTL_TEMP_SENSORS_GPU) populates hGPUTemp and writes its hardware maximum to goals.GPUThermalTemperatureMax. The VRAM sensor (CTL_TEMP_SENSORS_MEMORY) populates hMemTemp and writes its maximum to goals.GPUMemTempThreshold, clamped to [80°C, 120°C]. The B580 we used during testing reported a core maximum of 125°C and a VRAM maximum of 120°C — significantly higher than the 90°C fallback used when no sensor data is available, which underlines why querying the actual hardware ceiling matters rather than assuming a constant. 

Fan capability is gated through the fan handle enumeration. The code first attempts CTL_FAN_SPEED_UNITS_PERCENT on each fan handle. If that fails, it falls back to computing a percentage from RPM divided by the maxRPM property reported by ctlFanGetProperties(). If neither succeeds, bGPUFanReadingSupported remains false and Silent mode degrades gracefully to temperature-only regulation. The B580 returned MaxRPM: -1 from the properties call during our testing — a known issue with the current IGCL path for certain discrete cards. Fan speed is still visible in third-party tools, suggesting the correct API call differs from what we are using, and we are investigating. 

Power measurement uses IGCL's energy counter model rather than a direct wattage reading. ctlEnumPowerDomains() and ctlPowerGetEnergyCounter() return cumulative energy in Joules and a timestamp in microseconds. Power is derived from consecutive deltas: 

c 

double DeltaTime   = static_cast<double>(CurrTime   - PrevTime); 

double DeltaEnergy = static_cast<double>(CurrEnergy - PrevEnergy); 

// Energy in J, time in microseconds: W = J / (us / 1,000,000) 

return (DeltaTime > 0.0) ? static_cast<float>(DeltaEnergy / (DeltaTime / 1000000.0)) : 0.f; 

On the first call after initialisation, PrevTime is zero, so the function returns 0.f and stores the baseline counters. This is correct: there is no valid reference point for the first sample. 

Frequency domain and throttle reason detection is the most recent addition, prompted by Intel feedback asking why the system falls back to assumed temperature defaults rather than querying driver throttle state directly. The answer is that IGCL does expose this, via ctlFrequencyGetState() on the GPU frequency domain handle: 

c 

ctl_freq_state_t FreqState = {}; 

FreqState.Size = sizeof(ctl_freq_state_t); 

if (ctlFrequencyGetState(hFrequency, &FreqState) == CTL_RESULT_SUCCESS) 

{ 

    const uint32 NativeReasons = FreqState.throttleReasons; 

 

    if (NativeReasons & CTL_FREQ_THROTTLE_REASON_FLAG_THERMAL_LIMIT) 

    { 

        // Disambiguate: if VRAM is near its ceiling, it is a memory thermal limit 

        if (bGPUMemoryTemperatureSupported && MemTemp > (MaxMemTemp - 2.0f)) 

            UnifiedReasons |= (uint32)EGoalsGPUThrottleReason::ThermalVRAM; 

        else 

            UnifiedReasons |= (uint32)EGoalsGPUThrottleReason::ThermalCore; 

    } 

    if (NativeReasons & (CTL_FREQ_THROTTLE_REASON_FLAG_AVE_PWR_CAP | 

                         CTL_FREQ_THROTTLE_REASON_FLAG_BURST_PWR_CAP)) 

        UnifiedReasons |= (uint32)EGoalsGPUThrottleReason::PowerLimit; 

    if (NativeReasons & CTL_FREQ_THROTTLE_REASON_FLAG_CURRENT_LIMIT) 

        UnifiedReasons |= (uint32)EGoalsGPUThrottleReason::CurrentLimit; 

    if (NativeReasons & CTL_FREQ_THROTTLE_REASON_FLAG_PSU_ALERT) 

        UnifiedReasons |= (uint32)EGoalsGPUThrottleReason::External; 

} 

The throttle reason flags map to the same EGoalsGPUThrottleReason enum used by the NVIDIA path, which in turn feeds EvaluateThermalAdvisory(). This is a cross-vendor abstraction layer: ThermalCore, ThermalVRAM, PowerLimit, CurrentLimit, and External are vendor-neutral concepts that the subsystem can act on without knowing which GPU vendor reported them. 

 

The PID Controller: Adaptive Gain Scheduling 

The Temperature mode regulator uses adaptive gain scheduling that changes KP, KI, and KD based on how far the current temperature is from the target. 

For readers unfamiliar with PID controllers: a PID (Proportional-Integral-Derivative) controller is a feedback loop with three terms that together determine how aggressively to respond to being off-target. 

KP (Proportional) reacts to how far off you are right now. If the GPU is 10°C above target, KP pushes the frame rate down hard. If it's only 1°C above, KP nudges gently. It is the immediate response. 

KI (Integral) reacts to how long you have been off target. If the GPU has been running slightly too hot for a sustained period, the integral term accumulates and adds additional correction, catching cases where KP alone isn't strong enough to close a persistent gap. 

KD (Derivative) reacts to how fast the temperature is changing. If the GPU is heading toward the target quickly, the derivative term eases off the correction to avoid overshooting. It acts as a brake on momentum. 

Together the three terms give the controller both fast response to large deviations and fine-grained stability near the target. The practical goal here is a frame rate that settles smoothly around the thermal ceiling without oscillating, because oscillating frame rates are nearly as disruptive to competitive play as unconstrained heat. 

When the GPU is more than 4°C below the target (substantial thermal headroom), the controller switches to aggressive recovery gains and a 50% FPS smoothing factor, allowing frame rate to climb quickly back toward the sustainable maximum. If the error exceeds 8°C below target, the smoothed FPS ceiling is reset entirely — the device has cooled significantly and there is no reason to hold an artificially low cap. 

When the GPU is more than 4°C above the target, the controller switches to aggressive reduction gains with the same 50% smoothing factor, pulling frame rate down quickly. 

For small errors within ±4°C of target, the controller uses fine-tuning gains to make gentle adjustments and avoid oscillation: 

c 

if (Error < -4.0f)       // GPU is well under target — increase FPS aggressively 

{ 

    KP = 0.8f; KI = 0.1f; KD = 0.05f; 

    FPSSmoothfactor = 0.5f; 

} 

else if (Error > 4.0f)   // GPU is over target — reduce FPS aggressively 

{ 

    KP = 1.0f; KI = 0.2f; KD = 0.1f; 

    FPSSmoothfactor = 0.5f; 

} 

else                     // Small error — fine adjustments only 

{ 

    KP = 0.5f; KI = 0.05f; KD = 0.02f; 

} 

The integral term uses anti-windup decay (IntegralDecayFactor = 0.95f) to prevent accumulation during sustained deviation, and is clamped to ±5.0. The derivative term uses exponential smoothing (DerivativeSmoothingFactor = 0.1f) to suppress high-frequency noise in the temperature signal from causing erratic FPS adjustments. A ±1.5°C dead zone around the target suppresses unnecessary micro-adjustments when the system is already well-regulated. 

All gains are also exposed as console variable scalars (goals.GPUThrottleKPScale, KIScale, KDScale), allowing tuning without recompilation — useful both for development and for future scalability configuration per device profile. 

 

Thermal Pressure Scoring and Player Advisory 

Beyond frame rate regulation, the subsystem maintains a continuous thermal pressure score, a composite metric that drives player-facing notifications and informs other game systems. 

The score is computed as a weighted sum of two normalised signals: 

c 

LastPressureScore = (LastTempPressure * 0.7f) + (LastFanPressure * 0.3f); 

LastTempPressure is the current temperature expressed as a fraction of the hardware's maximum temperature threshold, queried from IGCL at initialisation. LastFanPressure is the current fan speed as a fraction of the highest level observed during the session, using a slow exponential decay (ObservedMaxFanLevel * 0.995f per update) to prevent a single early transient from permanently compressing the pressure range. 

When the pressure score exceeds the critical threshold (default 0.85) and has remained there for more than 12 seconds continuously, bGpuThermalCritical is set via the heuristic path. A separate VRAM pressure path tracks memory temperature against goals.GPUMemTempThreshold with a 10-second accumulation window. 

Crucially, the system also has a hardware augmentation layer that bypasses the time accumulation entirely. If ctlFrequencyGetState() reports active driver throttling due to thermal or power limits, EvaluateThermalAdvisory() forces bGpuThermalCritical true regardless of how long the pressure score has been elevated: 

c 

// Hardware Augmentation: if the driver confirms throttling, it is always critical 

if (bIsHardwareThermalThrottled || bIsHardwarePowerLimited || bIsHardwareCurrentLimited) 

    bGpuThermalCritical = true; 

if (bIsHardwareMemThermalThrottled) 

    bMemThermalCritical = true; 

This two-layer approach is important. The heuristic path catches gradual thermal buildup before the driver intervenes. The hardware path catches situations where the driver throttles at temperatures that are high but within specification — as intel engineers noted in our review, the discrete Intel B580's 125°C thermal ceiling means a player could be running at 120°C, well within spec from the driver's perspective, but potentially uncomfortably hot from a player's perspective if in an already warm room as noted at the start of this article. The user-configurable temperature targets in Silent and Temperature modes let players set their own comfort threshold well below the hardware maximum. 

This flag drives the in-game notification informing players that their hardware is under significant thermal load, giving them the context and agency to act rather than silently throttling the experience without explanation. 

 

What the Numbers Showed 

Internal testing compared throttled and non-throttled behaviour over a sustained gameplay session on a high-end desktop PC. The test was run using the same vendor-agnostic subsystem architecture described above, with the IGCL path active on Intel hardware and equivalent telemetry used on other configurations. These numbers were gathered during an earlier stage of development; we intend to re-run the comparison on a dedicated Intel Arc discrete GPU once additional test hardware is available, as the B580 testing confirmed the system works correctly on Intel hardware but did not produce thermal throttling events under normal gameplay load.  

Thomas_Hannaford_1-1781552503474.png

 

Thomas_Hannaford_2-1781552503475.png

 

 

No Throttling 

Temperature Mode (70°C target) 

Avg GPU Power 

~38W 

~26W 

Peak GPU Power 

45W+ 

45W+ 

Fan Speed 

4,800 RPM 

3,100 RPM 

Avg FPS 

95+ 

68 

Peak FPS 

115+ 

110+ 

 

The 32% reduction in average power draw is the headline figure, but the acoustic result matters more for the player experience. Fan RPM reduction from 4,800 to 3,100 RPM is audibly significant in a quiet room — the difference between a device that sounds like it is working hard and one that blends into the background. 

The temperature trace showed initial oscillation around the 70°C target before settling, expected behaviour from any feedback control system calibrating itself against real thermal inertia. After stabilisation, GPU temperature held at approximately 72°C for the duration of the session. 

 

How Players Actually Used It 

Live telemetry collected from players during testing showed that the majority of players who engaged with throttling settings chose Fixed FPS mode. This is unsurprising: Fixed FPS is a familiar concept with predictable and easily understood behaviour. Temperature and Silent modes require players to understand that frame rate is a consequence of thermal management rather than the target itself, a more abstract concept that the in-game UI didn't explain well enough at the time. 

The important finding, however, is that all throttling modes reduced average power consumption compared to no throttling, regardless of which mode players chose. 

Mode 

Users 

Power Samples 

Avg GPU Power 

No throttling 

18,885 

24,076,528 

Baseline 

Fixed FPS 

844 

1,638,563 

−5% 

Silent 

119 

206,813 

−6% 

Temperature 

144 

173,587 

−5.2% 

 

The baseline reduction from player-initiated throttling was meaningful even without optimised UX around the feature. This points to a clear opportunity: rather than leaving throttling off and requiring player discovery of the setting, there is a strong case for enabling Temperature mode by default on hardware capable of sustained high FPS, and surfacing it more prominently in the initial setup flow. 

 

Closing Thoughts 

The through-line connecting these systems is a single principle: treat frame rate and GPU load as variables to be managed, not targets to be maximised. 

On discrete GPUs, maximising GPU load is often fine because the thermal and acoustic consequences are confined to the GPU itself. On Intel Core Ultra processors, where CPU and GPU share a power budget and a memory bus, the consequences of an unmanaged GPU load propagate immediately and visibly into the player experience. 

Intel's hardware, the Xe GPU power model, the Core architecture, the IGCL telemetry API, gives developers the tools to manage this precisely. The question is whether you use them. 

While this first article focuses on discrete desktop performance, GPU Throttling brings a whole new dimension to systems with integrated GPUs (iGPUs) and a shared TDP. Part 2 of this article covers this in-depth, along with how GOALS handles handheld device profiles, XeSS upscaling and frame generation configuration, layered fallback for unknown hardware, a custom animation budget system with football-aware priority scoring, and battery vs. AC power state adaptation. 

 

GOALS launched in June 2026 on PlayStation, Xbox (including Xbox Game Pass), Steam, and Epic Games Store, with more platforms set to follow later this year. The GOALS team is a remote-first team spread across Europe, with HQ based in Stockholm, Sweden.  

 

Related resources: