From 448fc2cfe0f77f7cc087ebaf396b02994663a2a3 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Wed, 28 Oct 2026 15:17:14 -0400 Subject: [PATCH] soc: apple: clpc: give dense light GPU work the T8140 GPU floor On T8140 the GPU floor (DCS 4, fabric 3) follows graphics power: work that keeps graphics powered for 64 ms streams and gets it, while frame-paced loads power graphics up and down and leave it down. Light submissions a few milliseconds apart also let graphics power off between them, so they never stream, yet each of them waits on the fabric, which with the panel off nothing else holds up. Count the GPU driver's submissions through its activity input, and give the floor to work with two or more submissions in each of four sampling periods in a row (16 ms, one 60 Hz frame). A frame-paced load leaves a quieter period in every frame and stays below. The floor keeps the PMP back-off and the vote hold of streaming work. A publication does not trigger a sample of its own. One taken at the start of each frame of video playback finds the CPUs busy and holds the DCS up through their vote. debugfs counts the samples that applied the floor for streaming and for dense work. The T8140 GPU node now links its CLPC. Device trees without that link still find the single T8140 CLPC, as a hint the GPU probes without. (cherry picked from commit 264558de4e363c1cc748733ebea9987e857e51d3) [DjDeveloperr: without the T6040 host demand, the debugfs reason bits and the floor release counters, which this tree does not have; the sampler has no wake on a cold publication here, so there is none to skip] Signed-off-by: DjDeveloperr --- arch/arm64/boot/dts/apple/t8140.dtsi | 1 + drivers/soc/apple/clpc.c | 133 ++++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 4 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t8140.dtsi b/arch/arm64/boot/dts/apple/t8140.dtsi index bc008518b34634..9fdd91a302412d 100644 --- a/arch/arm64/boot/dts/apple/t8140.dtsi +++ b/arch/arm64/boot/dts/apple/t8140.dtsi @@ -1143,6 +1143,7 @@ <&uat_handoff>; memory-region-names = "ttbs", "pagetables", "handoff"; operating-points-v2 = <&gpu_opp>; + apple,clpc = <&clpc>; status = "disabled"; }; diff --git a/drivers/soc/apple/clpc.c b/drivers/soc/apple/clpc.c index 149a7cc8bf912c..f411ed8e6b83cf 100644 --- a/drivers/soc/apple/clpc.c +++ b/drivers/soc/apple/clpc.c @@ -69,6 +69,8 @@ enum apple_clpc_rail { * counts as streaming, see apple_clpc_gpu_streaming(); 0 counts it at once * @pmp_backoff: release that GPU floor while the PMP, voter agent * @pmp_agent, limits the DCS, see apple_clpc_gpu_limited() + * @gpu_dense: submissions per sampling period that make light GPU work + * dense, see apple_clpc_gpu_dense(); 0 for none */ struct apple_clpc_hw { unsigned int rail[APPLE_CLPC_RAILS]; @@ -78,6 +80,7 @@ struct apple_clpc_hw { unsigned int gpu_sustain_ms; bool pmp_backoff; unsigned int pmp_agent; + unsigned int gpu_dense; }; struct apple_clpc_cluster { @@ -136,9 +139,15 @@ struct apple_clpc { bool gpu_on; unsigned long gpu_on_since; unsigned long gpu_sampled; + /* Submissions seen at the last sample, and dense samples in a row */ + u64 gpu_sequence; + unsigned int gpu_dense_streak; /* Whether, and when, the PMP last limited the DCS while GPU work streamed */ bool gpu_limited; unsigned long gpu_limited_at; + /* Samples that applied the GPU floor for streaming or dense work. */ + unsigned long gpu_streaming_samples; + unsigned long gpu_dense_samples; }; /* Protects publication and removal of the drvdata, never a GPU report. */ @@ -199,10 +208,22 @@ int apple_clpc_gpu_get(struct device *dev, struct apple_clpc_gpu **out) struct platform_device *provider; struct apple_clpc *clpc; struct device_link *link; + bool fallback = false; int ret = -EPROBE_DEFER; *out = NULL; np = of_parse_phandle(dev->of_node, "apple,clpc", 0); + /* + * Device trees that predate the link on the T8140 GPU still have + * exactly one T8140 CLPC: take that one, as a hint the GPU can do + * without, so that it never waits for a CLPC that does not probe. + */ + if (!np && !of_find_property(dev->of_node, "apple,clpc", NULL) && + of_device_is_compatible(dev->of_node, "apple,agx-t8140")) { + np = of_find_compatible_node(NULL, NULL, "apple,t8140-clpc"); + fallback = true; + ret = 0; + } if (!np) return of_find_property(dev->of_node, "apple,clpc", NULL) ? -EINVAL : 0; if (!of_device_is_compatible(np, "apple,t8140-clpc")) { @@ -213,6 +234,10 @@ int apple_clpc_gpu_get(struct device *dev, struct apple_clpc_gpu **out) of_node_put(np); if (!provider) return ret; + if (fallback && !device_is_bound(&provider->dev)) { + put_device(&provider->dev); + return 0; + } link = device_link_add(dev, &provider->dev, DL_FLAG_AUTOREMOVE_CONSUMER); if (!link) { put_device(&provider->dev); @@ -406,6 +431,44 @@ static bool apple_clpc_gpu_streaming(struct apple_clpc *clpc, unsigned long now) msecs_to_jiffies(clpc->hw->gpu_sustain_ms)); } +/* Samples in a row that light GPU work must stay dense for: one 60 Hz frame. */ +#define APPLE_CLPC_GPU_DENSE_SAMPLES 4 + +/* + * Whether light GPU work is dense: at least gpu_dense submissions in each of + * the last GPU_DENSE_SAMPLES sampling periods. Such work keeps graphics + * powering up and down between submissions, so it never counts as streaming, + * yet each of its submissions waits on the fabric as streaming work does. + * Work paced by frames leaves a quieter period in every frame and stays below. + * @sequence counts every submission the GPU driver published; a sample late by + * more than one period ends a streak, as its count no longer covers one period. + */ +static bool apple_clpc_gpu_dense_step(unsigned int *streak, u64 *last, + u64 sequence, unsigned int threshold, + bool late) +{ + u64 submissions = sequence - *last; + + *last = sequence; + if (!threshold || late || submissions < threshold) + *streak = 0; + else if (*streak < APPLE_CLPC_GPU_DENSE_SAMPLES) + (*streak)++; + return *streak >= APPLE_CLPC_GPU_DENSE_SAMPLES; +} + +static bool apple_clpc_gpu_dense(struct apple_clpc *clpc, bool late) +{ + u64 updated, sequence; + + if (!clpc->gpu || !clpc->hw->gpu_dense) + return false; + apple_clpc_gpu_active(clpc->gpu, &updated, &sequence); + return apple_clpc_gpu_dense_step(&clpc->gpu_dense_streak, + &clpc->gpu_sequence, sequence, + clpc->hw->gpu_dense, late); +} + /* * Whether the PMP holds the GPU to a power budget: while GPU work streams, it * votes the DCS below the top state, which it does under sustained heavy @@ -453,10 +516,27 @@ static void apple_clpc_sample(struct apple_clpc *clpc) for (rail = 0; rail < APPLE_CLPC_RAILS; rail++) demand[rail] = max(demand[rail], cl->request[rail]); } - if (clpc->gpu_pmgr && apple_clpc_gpu_streaming(clpc, now) && - !apple_clpc_gpu_limited(clpc, now)) - for (rail = 0; rail < APPLE_CLPC_RAILS; rail++) - demand[rail] = max(demand[rail], clpc->hw->gpu[rail]); + if (clpc->gpu_pmgr) { + /* Both run every sample: each tracks state across samples. */ + bool late = time_after(now, clpc->gpu_sampled + + msecs_to_jiffies(2 * APPLE_CLPC_SAMPLE_MS)); + bool dense = apple_clpc_gpu_dense(clpc, late); + bool streaming = apple_clpc_gpu_streaming(clpc, now); + + if (streaming || dense) { + bool released = apple_clpc_gpu_limited(clpc, now); + + if (!released) { + if (streaming) + clpc->gpu_streaming_samples++; + else + clpc->gpu_dense_samples++; + for (rail = 0; rail < APPLE_CLPC_RAILS; rail++) + demand[rail] = max(demand[rail], + clpc->hw->gpu[rail]); + } + } + } for (rail = 0; rail < APPLE_CLPC_RAILS; rail++) state[rail] = apple_clpc_vote(&clpc->vote[rail], demand[rail], @@ -508,6 +588,8 @@ static int apple_clpc_status_show(struct seq_file *s, void *unused) jiffies_to_msecs(jiffies - clpc->gpu_limited_at)); else seq_puts(s, "never\n"); + seq_printf(s, "gpu floor samples: streaming %lu dense %lu\n", + clpc->gpu_streaming_samples, clpc->gpu_dense_samples); } if (clpc->gpu) { u64 updated, sequence; @@ -726,6 +808,12 @@ static const struct apple_clpc_hw apple_clpc_hw_t8132 = { * The PMP (agent 0) votes the DCS to its top state for streams of light GPU * submissions, and lowers it under sustained heavy rendering, which needs * neither rail's floor; see apple_clpc_gpu_limited(). + * + * Light submissions that come every few milliseconds let graphics power off + * between them, so they never stream, yet each still waits on the fabric, + * which with the panel off nothing else holds up. Two or more submissions in + * each of four sampling periods in a row (16 ms, one 60 Hz frame) get the + * floor too; a frame-paced load leaves a quieter period in every frame. */ static const struct apple_clpc_hw apple_clpc_hw_t8140 = { .rail = { [APPLE_CLPC_DCS] = 0, [APPLE_CLPC_SOC] = 1 }, @@ -738,6 +826,7 @@ static const struct apple_clpc_hw apple_clpc_hw_t8140 = { .gpu_sustain_ms = 64, .pmp_backoff = true, .pmp_agent = 0, + .gpu_dense = 2, }; #if IS_BUILTIN(CONFIG_KUNIT) && !defined(MODULE) @@ -784,7 +873,43 @@ static void apple_clpc_gpu_provider_removal_test(struct kunit *test) apple_clpc_gpu_put(gpu); } +static void apple_clpc_gpu_dense_test(struct kunit *test) +{ + unsigned int streak = 0, i; + u64 last = 0, sequence = 0; + + /* Two submissions in each of four periods in a row: dense. */ + for (i = 0; i < APPLE_CLPC_GPU_DENSE_SAMPLES - 1; i++) { + sequence += 2; + KUNIT_EXPECT_FALSE(test, apple_clpc_gpu_dense_step(&streak, &last, + sequence, 2, false)); + } + sequence += 2; + KUNIT_EXPECT_TRUE(test, apple_clpc_gpu_dense_step(&streak, &last, + sequence, 2, false)); + /* A quieter period, as every frame of a 60 Hz load has, ends it. */ + sequence += 1; + KUNIT_EXPECT_FALSE(test, apple_clpc_gpu_dense_step(&streak, &last, + sequence, 2, false)); + KUNIT_EXPECT_EQ(test, streak, 0u); + /* A late sample counts more than one period: it ends a streak too. */ + for (i = 0; i < APPLE_CLPC_GPU_DENSE_SAMPLES; i++) { + sequence += 4; + apple_clpc_gpu_dense_step(&streak, &last, sequence, 2, false); + } + KUNIT_EXPECT_EQ(test, streak, APPLE_CLPC_GPU_DENSE_SAMPLES); + sequence += 40; + KUNIT_EXPECT_FALSE(test, apple_clpc_gpu_dense_step(&streak, &last, + sequence, 2, true)); + /* No threshold: never dense. */ + streak = APPLE_CLPC_GPU_DENSE_SAMPLES; + sequence += 10; + KUNIT_EXPECT_FALSE(test, apple_clpc_gpu_dense_step(&streak, &last, + sequence, 0, false)); +} + static struct kunit_case apple_clpc_cases[] = { + KUNIT_CASE(apple_clpc_gpu_dense_test), KUNIT_CASE(apple_clpc_gpu_request_lifetime_test), KUNIT_CASE(apple_clpc_gpu_provider_removal_test), {}