From 22344bc0a7e473670463e6ccf27c0e74affd36d4 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Thu, 5 Nov 2026 12:07:51 -0500 Subject: [PATCH 1/3] dt-bindings: power: apple,pmc: Add display pipe interface enables On T8132 a display pipe votes for memory bandwidth and for a faster display clock through two PMC interfaces: bandwidth request (BWR) and boost. Each interface has an enable word with one bit per agent, and the PMC ignores the votes of a pipe whose bits are clear. macOS sets a pipe's two bits when the pipe's SYS power state powers on, and clears them before it powers off. Describe such a pair of bits as an interface-* child node of the PMC, provided as a power domain whose parent is the power state the enables need. The bits are set after the parent powers on and cleared before it powers off. Only the T8132 PMC has these words, so other PMCs may not have interface nodes. Signed-off-by: Erik Bourget --- .../devicetree/bindings/power/apple,pmc.yaml | 78 ++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/Documentation/devicetree/bindings/power/apple,pmc.yaml b/Documentation/devicetree/bindings/power/apple,pmc.yaml index 4c1d9aefc2519..a3728ed3effee 100644 --- a/Documentation/devicetree/bindings/power/apple,pmc.yaml +++ b/Documentation/devicetree/bindings/power/apple,pmc.yaml @@ -35,8 +35,10 @@ properties: description: | The perf-state floor words of the voter agents ("voters"), and the per-agent words holding the floor the PMC honours for each agent with - the per-rail interface enable words ("rails"). Optionally, the cluster - enable control word followed by its status word ("clusters"). + the per-rail interface enable words ("rails"); on T8132 the rails + block also holds the bandwidth request and boost interface enable + words. Optionally, the cluster enable control word followed by its + status word ("clusters"). reg-names: description: | @@ -178,6 +180,43 @@ patternProperties: - '#power-domain-cells' - apple,cluster-bit + '^interface-[a-z0-9-]+$': + type: object + description: | + A display pipe's interface enables, provided as a power domain. The + bandwidth request (BWR) and boost interfaces each have an enable word + with one bit per agent, and the PMC ignores the pipe's bandwidth and + boost votes while its bits are clear. Both bits are set after the + parent domain powers on, and cleared before it powers off. + additionalProperties: false + + properties: + label: + description: Name of the power domain. + + '#power-domain-cells': + const: 0 + + power-domains: + maxItems: 1 + description: Domain that must be powered while the enables are set. + + apple,bwr-bit: + $ref: /schemas/types.yaml#/definitions/uint32 + maximum: 31 + description: Bit of the pipe in the BWR interface enable word. + + apple,boost-bit: + $ref: /schemas/types.yaml#/definitions/uint32 + maximum: 31 + description: Bit of the pipe in the boost interface enable word. + + required: + - '#power-domain-cells' + - power-domains + - apple,bwr-bit + - apple,boost-bit + '^performance-controller@[0-9a-f]+$': type: object description: The host performance controller's voter agent. @@ -207,6 +246,17 @@ required: - reg - reg-names +allOf: + - if: + properties: + compatible: + not: + contains: + const: apple,t8132-pmc + then: + patternProperties: + '^interface-[a-z0-9-]+$': false + additionalProperties: false examples: @@ -245,3 +295,27 @@ examples: }; }; }; + + - | + soc { + #address-cells = <2>; + #size-cells = <2>; + + power-management@382800000 { + compatible = "apple,t8132-pmc"; + reg = <0x3 0x82800000 0x0 0x10000>, + <0x3 0x828aa000 0x0 0x3000>; + reg-names = "voters", "rails"; + #address-cells = <1>; + #size-cells = <0>; + apple,static-voters = <3>; + + interface-dispext0 { + label = "dispext0-interface"; + #power-domain-cells = <0>; + power-domains = <&ps_dispext0_sys>; + apple,bwr-bit = <3>; + apple,boost-bit = <1>; + }; + }; + }; From c1db3bc458e7590eace43ee03021d61b8ea22eda Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Thu, 5 Nov 2026 12:07:51 -0500 Subject: [PATCH 2/3] pmdomain: apple: pmc: provide display pipe interface enables as power domains The T8132 PMC ignores an external display pipe's bandwidth and boost votes until the pipe's bits are set in the BWR and boost interface enable words. With them clear, the pipe's display clock stays at its lowest state, and a 4K mode at 120 Hz shows only the first lines of each frame. macOS sets the bits when the pipe's SYS power state powers on, and clears them before it powers off. Add the offsets of the two enable words in the "rails" region to the per-SoC layout: 0x2020 and 0x2028 on T8132, and 0 elsewhere, where an interface node is rejected. Provide each interface-* child node as a power domain that joins its parent domain like the cluster domains do. Power on sets the pipe's boost bit and then its BWR bit, and power off clears them, each with a read-modify-write under the PMC lock that is read back. If the BWR word does not take the change, the boost bit gets back the value it had before, and the call fails with -EIO. A bit claimed by two domains is rejected. The domain is IRQ safe: the PMGR power states that depend on it are, and genpd refuses an IRQ-safe subdomain under a parent that is not. The callbacks only take the PMC's raw spinlock and access MMIO. Like a voter domain, an interface domain starts from the hardware state: it is on if both of its bits are already set. With only one bit set it starts off and that bit is left alone until the domain powers on, which sets the other one; a failed power on keeps it as well. So the driver never drops a vote the boot loader left before the pipe powers off. When the PMC goes away, the domains' bits get back their values at probe, as the rail enables do. System sleep needs nothing extra: genpd powers the domain off and on with the domains that depend on it, and the words, like the rail enables, are not saved and restored. Show both words in the debugfs status, and add KUnit cases for setting and clearing a pipe's bits next to the internal display's boost bit, for a BWR word that does not take the change, for the boot states, and for the domain linked between IRQ-safe SYS and FE power states the way the PMGR driver links them, powered through a device on FE. Signed-off-by: Erik Bourget --- drivers/pmdomain/apple/Kconfig | 5 +- drivers/pmdomain/apple/pmc-test.c | 208 +++++++++++++++++++++++++++++ drivers/pmdomain/apple/pmc.c | 213 ++++++++++++++++++++++++++++++ 3 files changed, 424 insertions(+), 2 deletions(-) diff --git a/drivers/pmdomain/apple/Kconfig b/drivers/pmdomain/apple/Kconfig index c177665e17a71..52fe2669e6fe8 100644 --- a/drivers/pmdomain/apple/Kconfig +++ b/drivers/pmdomain/apple/Kconfig @@ -74,8 +74,9 @@ config APPLE_PMC shared rails, such as the DRAM controller and the SoC fabric, between voter agents. This driver enables the static agents and provides power domains that enable a dynamic agent, such as the - GPU, while its device is powered on. It can also limit the DRAM - controller rail as a thermal cooling device. + GPU, or a display pipe's bandwidth and boost interfaces, while + its device is powered on. It can also limit the DRAM controller + rail as a thermal cooling device. config APPLE_PMC_KUNIT_TEST bool "KUnit tests for the Apple PMC voter interface" if !KUNIT_ALL_TESTS diff --git a/drivers/pmdomain/apple/pmc-test.c b/drivers/pmdomain/apple/pmc-test.c index 7cf8a43e8b085..d563a178dc0e8 100644 --- a/drivers/pmdomain/apple/pmc-test.c +++ b/drivers/pmdomain/apple/pmc-test.c @@ -8,6 +8,7 @@ #include #include +#include #define APPLE_PMC_TEST_RAILS_SIZE 0x4000 @@ -281,6 +282,209 @@ static void apple_pmc_test_dcs_floor(struct kunit *test) KUNIT_EXPECT_EQ(test, apple_pmc_test_ap_floor(pmc) & ~APPLE_PMC_RAIL_MASK(hw->dcs_rail), 0U); } +/* + * A T8132 interface domain sets and clears its own BWR and boost enables + * only: the internal display's boost enable, bit 0, stays set. + */ +static void apple_pmc_test_interface(struct kunit *test) +{ + struct apple_pmc *pmc = apple_pmc_test_alloc(test); + struct apple_pmc_domain dom = { + .pmc = pmc, .interface = true, .bwr_bit = 3, .boost_bit = 1, + }; + const struct apple_pmc_hw *hw = &apple_pmc_hw_t8132; + + pmc->hw = hw; + writel(BIT(0), pmc->rails + hw->boost_enable); + + KUNIT_ASSERT_EQ(test, apple_pmc_domain_power_on(&dom.genpd), 0); + KUNIT_EXPECT_EQ(test, readl(pmc->rails + hw->bwr_enable), BIT(3)); + KUNIT_EXPECT_EQ(test, readl(pmc->rails + hw->boost_enable), BIT(0) | BIT(1)); + KUNIT_ASSERT_EQ(test, apple_pmc_domain_power_off(&dom.genpd), 0); + KUNIT_EXPECT_EQ(test, readl(pmc->rails + hw->bwr_enable), 0U); + KUNIT_EXPECT_EQ(test, readl(pmc->rails + hw->boost_enable), BIT(0)); +} + +struct apple_pmc_test_stuck { + u32 offset; + u32 value; +}; + +/* The interface enable word at stuck->offset keeps stuck->value. */ +static u32 apple_pmc_test_stuck_read(struct apple_pmc *pmc, u32 offset) +{ + struct kunit *test = kunit_get_current_test(); + struct apple_pmc_test_stuck *stuck = test->priv; + + if (offset == stuck->offset) + writel(stuck->value, pmc->rails + offset); + return readl(pmc->rails + offset); +} + +/* + * If the BWR enable does not change, the boost enable goes back too, both + * when the domain powers on and when it powers off. + */ +static void apple_pmc_test_interface_stuck(struct kunit *test) +{ + struct apple_pmc *pmc = apple_pmc_test_alloc(test); + struct apple_pmc_domain dom = { + .pmc = pmc, .interface = true, .bwr_bit = 3, .boost_bit = 1, + }; + const struct apple_pmc_hw *hw = &apple_pmc_hw_t8132; + struct apple_pmc_test_stuck stuck = { .offset = hw->bwr_enable }; + + pmc->hw = hw; + writel(BIT(0), pmc->rails + hw->boost_enable); + test->priv = &stuck; + kunit_activate_static_stub(test, apple_pmc_interface_read, + apple_pmc_test_stuck_read); + + KUNIT_EXPECT_EQ(test, apple_pmc_domain_power_on(&dom.genpd), -EIO); + KUNIT_EXPECT_EQ(test, apple_pmc_interface_read(pmc, hw->bwr_enable), 0U); + KUNIT_EXPECT_EQ(test, apple_pmc_interface_read(pmc, hw->boost_enable), BIT(0)); + + stuck.value = BIT(3); + writel(BIT(0) | BIT(1), pmc->rails + hw->boost_enable); + KUNIT_EXPECT_EQ(test, apple_pmc_domain_power_off(&dom.genpd), -EIO); + KUNIT_EXPECT_EQ(test, apple_pmc_interface_read(pmc, hw->bwr_enable), BIT(3)); + KUNIT_EXPECT_EQ(test, apple_pmc_interface_read(pmc, hw->boost_enable), + BIT(0) | BIT(1)); + + kunit_deactivate_static_stub(test, apple_pmc_interface_read); + test->priv = NULL; +} + +static void apple_pmc_test_genpd_remove(void *genpd) +{ + pm_genpd_remove(genpd); +} + +static void apple_pmc_test_genpd_detach(void *dev) +{ + pm_runtime_disable(dev); + pm_genpd_remove_device(dev); +} + +/* A genpd that is removed again when the test ends, children first. */ +static struct generic_pm_domain *apple_pmc_test_genpd(struct kunit *test, + struct generic_pm_domain *genpd, + int ret) +{ + KUNIT_ASSERT_EQ(test, ret, 0); + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, + apple_pmc_test_genpd_remove, + genpd), 0); + return genpd; +} + +static struct apple_pmc_domain *apple_pmc_test_interface_dom(struct kunit *test, + struct apple_pmc *pmc, + const char *name) +{ + struct apple_pmc_domain *dom = kunit_kzalloc(test, sizeof(*dom), GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, dom); + dom->pmc = pmc; + dom->interface = true; + dom->bwr_bit = 3; + dom->boost_bit = 1; + dom->genpd.name = name; + apple_pmc_test_genpd(test, &dom->genpd, apple_pmc_interface_init(pmc, dom)); + return dom; +} + +/* + * Link the interface domain as the PMGR driver does: under a SYS power + * state, with an FE power state below both, all of them IRQ safe. Powering + * FE through a device sets the enables after SYS is on, and powering it + * off clears them. + */ +static void apple_pmc_test_interface_genpd(struct kunit *test) +{ + struct apple_pmc *pmc = apple_pmc_test_alloc(test); + const struct apple_pmc_hw *hw = &apple_pmc_hw_t8132; + struct generic_pm_domain *sys, *fe; + struct apple_pmc_domain *dom; + struct device *dev; + + pmc->hw = hw; + writel(BIT(0), pmc->rails + hw->boost_enable); + pmc->saved_boost = BIT(0); + dev = kunit_device_register(test, "apple-pmc-test-fe"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev); + + sys = kunit_kzalloc(test, sizeof(*sys), GFP_KERNEL); + fe = kunit_kzalloc(test, sizeof(*fe), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, sys); + KUNIT_ASSERT_NOT_NULL(test, fe); + sys->name = "apple-pmc-test-sys"; + sys->flags = GENPD_FLAG_IRQ_SAFE; + fe->name = "apple-pmc-test-fe"; + fe->flags = GENPD_FLAG_IRQ_SAFE; + apple_pmc_test_genpd(test, sys, pm_genpd_init(sys, NULL, false)); + dom = apple_pmc_test_interface_dom(test, pmc, "apple-pmc-test-interface"); + KUNIT_EXPECT_EQ(test, dom->genpd.status, GENPD_STATE_OFF); + apple_pmc_test_genpd(test, fe, pm_genpd_init(fe, NULL, true)); + + KUNIT_ASSERT_EQ(test, pm_genpd_add_subdomain(sys, &dom->genpd), 0); + KUNIT_ASSERT_EQ(test, pm_genpd_add_subdomain(sys, fe), 0); + KUNIT_ASSERT_EQ(test, pm_genpd_add_subdomain(&dom->genpd, fe), 0); + + KUNIT_ASSERT_EQ(test, pm_genpd_add_device(fe, dev), 0); + pm_runtime_enable(dev); + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, + apple_pmc_test_genpd_detach, + dev), 0); + + KUNIT_ASSERT_EQ(test, pm_runtime_resume_and_get(dev), 0); + KUNIT_EXPECT_EQ(test, dom->genpd.status, GENPD_STATE_ON); + KUNIT_EXPECT_EQ(test, readl(pmc->rails + hw->bwr_enable), BIT(3)); + KUNIT_EXPECT_EQ(test, readl(pmc->rails + hw->boost_enable), BIT(0) | BIT(1)); + + KUNIT_EXPECT_EQ(test, pm_runtime_put_sync_suspend(dev), 0); + KUNIT_EXPECT_EQ(test, dom->genpd.status, GENPD_STATE_OFF); + KUNIT_EXPECT_EQ(test, readl(pmc->rails + hw->bwr_enable), 0U); + KUNIT_EXPECT_EQ(test, readl(pmc->rails + hw->boost_enable), BIT(0)); +} + +/* + * A pipe with both enables set at boot starts on. One with only its boost + * enable set starts off, and a power on that fails on the BWR enable keeps + * that boost enable instead of clearing it. + */ +static void apple_pmc_test_interface_boot_state(struct kunit *test) +{ + struct apple_pmc *pmc = apple_pmc_test_alloc(test); + const struct apple_pmc_hw *hw = &apple_pmc_hw_t8132; + struct apple_pmc_test_stuck stuck = { .offset = hw->bwr_enable }; + struct apple_pmc_domain *dom; + + pmc->hw = hw; + pmc->saved_bwr = BIT(3); + pmc->saved_boost = BIT(0) | BIT(1); + dom = apple_pmc_test_interface_dom(test, pmc, "apple-pmc-test-on"); + KUNIT_EXPECT_EQ(test, dom->genpd.status, GENPD_STATE_ON); + + pmc->saved_bwr = 0; + writel(BIT(0) | BIT(1), pmc->rails + hw->boost_enable); + dom = apple_pmc_test_interface_dom(test, pmc, "apple-pmc-test-mixed"); + KUNIT_EXPECT_EQ(test, dom->genpd.status, GENPD_STATE_OFF); + + test->priv = &stuck; + kunit_activate_static_stub(test, apple_pmc_interface_read, + apple_pmc_test_stuck_read); + KUNIT_EXPECT_EQ(test, apple_pmc_domain_power_on(&dom->genpd), -EIO); + KUNIT_EXPECT_EQ(test, apple_pmc_interface_read(pmc, hw->boost_enable), + BIT(0) | BIT(1)); + kunit_deactivate_static_stub(test, apple_pmc_interface_read); + test->priv = NULL; + + KUNIT_EXPECT_EQ(test, apple_pmc_domain_power_on(&dom->genpd), 0); + KUNIT_EXPECT_EQ(test, readl(pmc->rails + hw->bwr_enable), BIT(3)); + KUNIT_EXPECT_EQ(test, readl(pmc->rails + hw->boost_enable), BIT(0) | BIT(1)); +} + static struct kunit_case apple_pmc_test_cases[] = { KUNIT_CASE(apple_pmc_test_dcs_floor), KUNIT_CASE(apple_pmc_test_partial_enable), @@ -291,6 +495,10 @@ static struct kunit_case apple_pmc_test_cases[] = { KUNIT_CASE(apple_pmc_test_dcs_limit_restores_ap_vote), KUNIT_CASE(apple_pmc_test_floor_aggregate), KUNIT_CASE(apple_pmc_test_floor_pending_timeout), + KUNIT_CASE(apple_pmc_test_interface), + KUNIT_CASE(apple_pmc_test_interface_stuck), + KUNIT_CASE(apple_pmc_test_interface_genpd), + KUNIT_CASE(apple_pmc_test_interface_boot_state), {} }; diff --git a/drivers/pmdomain/apple/pmc.c b/drivers/pmdomain/apple/pmc.c index 6b1e4c472e399..b6b5ccdb3e53e 100644 --- a/drivers/pmdomain/apple/pmc.c +++ b/drivers/pmdomain/apple/pmc.c @@ -23,6 +23,15 @@ * at its highest state to encode at full speed; macOS raises the same rail * for every encode session. Floors on one rail combine to the highest. * + * On T8132 a display pipe votes through two more interfaces: bandwidth + * requests (BWR) and boost, which raises the display clock. Each has an + * enable word with one bit per agent, and the PMC ignores the votes of an + * agent whose bit is clear: the external pipes' display clock then stays at + * its lowest state, too slow for modes such as 4K at 120 Hz. macOS sets a + * pipe's two bits when its PMGR power state powers on, and clears them + * before it powers off. The driver models them as a power domain too, that + * sits between that power state and the ones that depend on it. + * * Under thermal pressure the driver can also limit the DRAM controller * (DCS) rail: it stops honouring the other agents on that rail and votes * on their behalf through the AP's own agent, capped. This bounds the heat @@ -75,6 +84,10 @@ * @agent_rails: rails on which an agent is enabled; 0 for every rail * @ap_agent: voter agent of the application processor, which a DCS limit * votes through + * @bwr_enable: offset of the bandwidth request (BWR) interface enable word + * in the "rails" region; 0 if the PMC has none + * @boost_enable: offset of the boost interface enable word in the "rails" + * region; 0 if the PMC has none */ struct apple_pmc_hw { u32 agent_stride; @@ -87,6 +100,8 @@ struct apple_pmc_hw { u32 agent_rails; u32 ap_agent; bool has_pending; + u32 bwr_enable; + u32 boost_enable; }; struct apple_pmc { @@ -107,6 +122,11 @@ struct apple_pmc { u32 static_agents; /* Rails with a floor domain. */ u32 floor_rails; + /* BWR and boost interface enables at probe, and the bits of domains. */ + u32 saved_bwr; + u32 saved_boost; + u32 bwr_bits; + u32 boost_bits; struct list_head domains; struct dentry *debugfs; /* Cooling state of the DCS limit: 0 is none, dcs_max holds DCS at 0. */ @@ -129,6 +149,9 @@ struct apple_pmc { * @on: with @floor, whether the floor is held; protected by pmc->lock * @cluster: the domain is a cluster enable bit in the "clusters" region * @bit: with @cluster, the bit in the control and status words + * @interface: the domain is a display pipe's BWR and boost interface enables + * @bwr_bit: with @interface, the bit in the BWR interface enable word + * @boost_bit: with @interface, the bit in the boost interface enable word */ struct apple_pmc_domain { struct generic_pm_domain genpd; @@ -142,6 +165,9 @@ struct apple_pmc_domain { bool on; bool cluster; unsigned int bit; + bool interface; + unsigned int bwr_bit; + unsigned int boost_bit; bool provider; }; @@ -678,10 +704,74 @@ static int apple_pmc_cluster_set(struct apple_pmc_domain *dom, bool on) return ret; } +static u32 apple_pmc_interface_read(struct apple_pmc *pmc, u32 offset) +{ +#if IS_ENABLED(CONFIG_APPLE_PMC_KUNIT_TEST) + KUNIT_STATIC_STUB_REDIRECT(apple_pmc_interface_read, pmc, offset); +#endif + return readl(pmc->rails + offset); +} + +/* + * Caller holds pmc->lock. Set or clear @bit in the interface enable word at + * @offset, and read the word back. + */ +static int apple_pmc_interface_update(struct apple_pmc *pmc, u32 offset, + unsigned int bit, bool on) +{ + u32 old = apple_pmc_interface_read(pmc, offset); + u32 val = on ? old | BIT(bit) : old & ~BIT(bit); + + if (val == old) + return 0; + + writel(val, pmc->rails + offset); + if ((apple_pmc_interface_read(pmc, offset) ^ val) & BIT(bit)) { + dev_err_ratelimited(pmc->dev, + "interface enable %#x bit %u did not turn %s\n", + offset, bit, str_on_off(on)); + return -EIO; + } + + return 0; +} + +/* + * Set or clear the boost and BWR enables of a display pipe. If the BWR + * enable does not take the change, put the boost enable back to what it + * was: genpd keeps the domain in its old state when a callback fails, and + * a boost enable that was already set, such as one the boot loader left + * for a running pipe, must not be lost to a failed power on. Called with + * IRQs off for an IRQ-safe domain; everything here is atomic. + */ +static int apple_pmc_interface_set(struct apple_pmc_domain *dom, bool on) +{ + struct apple_pmc *pmc = dom->pmc; + const struct apple_pmc_hw *hw = pmc->hw; + unsigned long flags; + bool boost; + int ret; + + raw_spin_lock_irqsave(&pmc->lock, flags); + boost = apple_pmc_interface_read(pmc, hw->boost_enable) & BIT(dom->boost_bit); + ret = apple_pmc_interface_update(pmc, hw->boost_enable, dom->boost_bit, on); + if (!ret) { + ret = apple_pmc_interface_update(pmc, hw->bwr_enable, dom->bwr_bit, on); + if (ret) + apple_pmc_interface_update(pmc, hw->boost_enable, + dom->boost_bit, boost); + } + raw_spin_unlock_irqrestore(&pmc->lock, flags); + + return ret; +} + static int apple_pmc_domain_power_on(struct generic_pm_domain *genpd) { struct apple_pmc_domain *dom = genpd_to_apple_pmc_domain(genpd); + if (dom->interface) + return apple_pmc_interface_set(dom, true); if (dom->cluster) return apple_pmc_cluster_set(dom, true); if (dom->floor) @@ -693,6 +783,8 @@ static int apple_pmc_domain_power_off(struct generic_pm_domain *genpd) { struct apple_pmc_domain *dom = genpd_to_apple_pmc_domain(genpd); + if (dom->interface) + return apple_pmc_interface_set(dom, false); if (dom->cluster) return apple_pmc_cluster_set(dom, false); if (dom->floor) @@ -857,6 +949,100 @@ static int apple_pmc_cluster_add(struct apple_pmc *pmc, struct device_node *np) return 0; } +/* + * Initialize the genpd of an interface domain, which starts from the + * hardware state: on if both of its bits are set, so that the enables of a + * pipe the boot loader left running stay set. With only one bit set it + * starts off, and the driver leaves that bit alone until the domain powers + * on, which sets the other one. A failed power on keeps the bit too, so no + * vote the boot loader left is dropped before the pipe powers off. + * + * The domain is IRQ safe, since the PMGR power states that depend on it + * are, and genpd only takes an IRQ-safe subdomain under an IRQ-safe parent. + */ +static int apple_pmc_interface_init(struct apple_pmc *pmc, + struct apple_pmc_domain *dom) +{ + bool on = (pmc->saved_bwr & BIT(dom->bwr_bit)) && + (pmc->saved_boost & BIT(dom->boost_bit)); + + dom->genpd.flags |= GENPD_FLAG_IRQ_SAFE; + dom->genpd.power_on = apple_pmc_domain_power_on; + dom->genpd.power_off = apple_pmc_domain_power_off; + + return pm_genpd_init(&dom->genpd, NULL, !on); +} + +/* A display pipe's BWR and boost interface enables, set while powered. */ +static int apple_pmc_interface_add(struct apple_pmc *pmc, struct device_node *np) +{ + const struct apple_pmc_hw *hw = pmc->hw; + struct device *dev = pmc->dev; + struct apple_pmc_domain *dom; + struct of_phandle_args parent; + const char *label; + u32 bwr, boost; + int i, ret; + + if (!hw->bwr_enable || !hw->boost_enable) + return dev_err_probe(dev, -EINVAL, + "%pOF: no interface enables on this PMC\n", np); + if (of_property_read_u32(np, "apple,bwr-bit", &bwr) || bwr >= 32 || + of_property_read_u32(np, "apple,boost-bit", &boost) || boost >= 32) + return dev_err_probe(dev, -EINVAL, "%pOF: invalid interface bits\n", np); + if ((pmc->bwr_bits & BIT(bwr)) || (pmc->boost_bits & BIT(boost))) + return dev_err_probe(dev, -EINVAL, "%pOF: interface bits in use\n", np); + + dom = kzalloc_obj(*dom); + if (!dom) + return -ENOMEM; + + dom->pmc = pmc; + dom->np = of_node_get(np); + dom->interface = true; + dom->bwr_bit = bwr; + dom->boost_bit = boost; + if (of_property_read_string(np, "label", &label)) + label = np->name; + dom->genpd.name = kasprintf(GFP_KERNEL, "pmc-%s", label); + if (!dom->genpd.name) { + of_node_put(dom->np); + kfree(dom); + return -ENOMEM; + } + + ret = apple_pmc_interface_init(pmc, dom); + if (ret) { + of_node_put(dom->np); + kfree(dom->genpd.name); + kfree(dom); + return dev_err_probe(dev, ret, "%pOF: genpd init failed\n", np); + } + list_add_tail(&dom->list, &pmc->domains); + pmc->bwr_bits |= BIT(bwr); + pmc->boost_bits |= BIT(boost); + + ret = of_genpd_add_provider_simple(np, &dom->genpd); + if (ret) + return dev_err_probe(dev, ret, "%pOF: provider failed\n", np); + dom->provider = true; + + for (i = 0; !of_parse_phandle_with_args(np, "power-domains", + "#power-domain-cells", i, + &parent); i++) { + struct of_phandle_args child = { .np = np }; + + ret = of_genpd_add_subdomain(&parent, &child); + of_node_put(parent.np); + if (ret) + return dev_err_probe(dev, ret, + "%pOF: failed to join parent domain\n", + np); + } + + return 0; +} + /* A rail floor held through the AP's agent while the domain is powered. */ static int apple_pmc_floor_add(struct apple_pmc *pmc, struct device_node *np) { @@ -936,6 +1122,14 @@ static int apple_pmc_status_show(struct seq_file *s, void *unused) i, apple_pmc_enable_read(pmc, i), pmc->saved_enable[i]); seq_printf(s, "dcs limit: state %lu of %u\n", READ_ONCE(pmc->dcs_state), hw->dcs_max); + if (hw->bwr_enable && hw->boost_enable) { + seq_printf(s, "bwr enable: %#010x (saved %#010x)\n", + apple_pmc_interface_read(pmc, hw->bwr_enable), + pmc->saved_bwr); + seq_printf(s, "boost enable: %#010x (saved %#010x)\n", + apple_pmc_interface_read(pmc, hw->boost_enable), + pmc->saved_boost); + } list_for_each_entry(dom, &pmc->domains, list) if (dom->floor) seq_printf(s, "%s: rail%u level %u %s\n", dom->genpd.name, @@ -970,6 +1164,16 @@ static void apple_pmc_release(void *data) for (rail = 0; rail < pmc->hw->rails; rail++) writel(pmc->saved_enable[rail], pmc->rails + pmc->hw->rail_enable + 4 * rail); + /* Nor does an interface enable: give the domains' bits back. */ + if (pmc->bwr_bits || pmc->boost_bits) { + void __iomem *bwr = pmc->rails + pmc->hw->bwr_enable; + void __iomem *boost = pmc->rails + pmc->hw->boost_enable; + + writel((readl(bwr) & ~pmc->bwr_bits) | + (pmc->saved_bwr & pmc->bwr_bits), bwr); + writel((readl(boost) & ~pmc->boost_bits) | + (pmc->saved_boost & pmc->boost_bits), boost); + } pmc->dcs_state = 0; apple_pmc_dcs_vote(pmc); raw_spin_unlock_irqrestore(&pmc->lock, flags); @@ -1037,6 +1241,10 @@ static int apple_pmc_probe(struct platform_device *pdev) "PMC is not responding\n"); pmc->enable[rail] = pmc->saved_enable[rail]; } + if (pmc->hw->bwr_enable && pmc->hw->boost_enable) { + pmc->saved_bwr = apple_pmc_interface_read(pmc, pmc->hw->bwr_enable); + pmc->saved_boost = apple_pmc_interface_read(pmc, pmc->hw->boost_enable); + } pmc->ap_dcs_vote = apple_pmc_ap_dcs_read(pmc); count = of_property_count_u32_elems(np, "apple,static-voters"); @@ -1078,6 +1286,9 @@ static int apple_pmc_probe(struct platform_device *pdev) ret = apple_pmc_floor_add(pmc, child); else if (of_property_present(child, "apple,cluster-bit")) ret = apple_pmc_cluster_add(pmc, child); + else if (of_property_present(child, "apple,bwr-bit") || + of_property_present(child, "apple,boost-bit")) + ret = apple_pmc_interface_add(pmc, child); else ret = apple_pmc_domain_add(pmc, child); if (ret) { @@ -1111,6 +1322,8 @@ static const struct apple_pmc_hw apple_pmc_hw_t8132 = { .dcs_max = 5, .ap_agent = 3, .has_pending = true, + .bwr_enable = 0x2020, + .boost_enable = 0x2028, }; static const struct apple_pmc_hw apple_pmc_hw_t8140 = { From 1d9550362747606aa570f48a35aab81eb589c7ad Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Thu, 5 Nov 2026 12:07:51 -0500 Subject: [PATCH 3/3] arm64: dts: apple: t8132: Add the external display PMC interface enables The PMC ignores the bandwidth and boost votes of an external display pipe until the pipe's BWR and boost interface enables are set, so the pipe's display clock stays at its lowest state and a 4K mode at 120 Hz shows only the first lines of each frame. Describe the enables of dispext0 (BWR bit 3, boost bit 1) and dispext1 (BWR bit 4, boost bit 2) under the PMC, each with the pipe's SYS power state as its parent. On the J713, where the PMC is enabled, make each pipe's FE power state depend on them as well as on SYS. genpd then powers on SYS, the enables and FE in that order, and powers them off in reverse, so the enables are only set while SYS is on. The other T8132 boards keep the PMC disabled, so the dependency lives in the J713 DT and not in t8132-pmgr.dtsi, where it would hold their FE power states back for good. The legacy-loader adapter copies the t8132-memory.dtsi nodes, phandles included, into a loader tree that lacks them. The interface nodes' parents do not resolve there, and the loader's PMGR states do not use the nodes, so drop them from the adapter's data. Signed-off-by: Erik Bourget --- arch/arm64/boot/dts/apple/t8132-j713.dts | 12 ++++++++++++ arch/arm64/boot/dts/apple/t8132-memory.dtsi | 21 +++++++++++++++++++++ drivers/soc/apple/t8132-platform-data.dts | 7 +++++++ 3 files changed, 40 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t8132-j713.dts b/arch/arm64/boot/dts/apple/t8132-j713.dts index f84c4ef92aac1..d72442c6797f0 100644 --- a/arch/arm64/boot/dts/apple/t8132-j713.dts +++ b/arch/arm64/boot/dts/apple/t8132-j713.dts @@ -540,6 +540,18 @@ apple,static-voters = <3>; }; +/* + * The PMC enables of an external pipe are set once the pipe's SYS state is + * on, and before its FE state powers on. + */ +&ps_dispext0_fe { + power-domains = <&ps_dispext0_sys>, <&pmc_dispext0>; +}; + +&ps_dispext1_fe { + power-domains = <&ps_dispext1_sys>, <&pmc_dispext1>; +}; + &smc { status = "okay"; diff --git a/arch/arm64/boot/dts/apple/t8132-memory.dtsi b/arch/arm64/boot/dts/apple/t8132-memory.dtsi index fea5b3a97dbf5..c6cafa8219d2a 100644 --- a/arch/arm64/boot/dts/apple/t8132-memory.dtsi +++ b/arch/arm64/boot/dts/apple/t8132-memory.dtsi @@ -19,6 +19,27 @@ apple,pmp = <&pmp>; status = "disabled"; + /* + * The external display pipes' bandwidth request (BWR) and + * boost interface enables. The PMC ignores a pipe's bandwidth + * and display clock votes while they are clear. + */ + pmc_dispext0: interface-dispext0 { + label = "dispext0-interface"; + #power-domain-cells = <0>; + power-domains = <&ps_dispext0_sys>; + apple,bwr-bit = <3>; + apple,boost-bit = <1>; + }; + + pmc_dispext1: interface-dispext1 { + label = "dispext1-interface"; + #power-domain-cells = <0>; + power-domains = <&ps_dispext1_sys>; + apple,bwr-bit = <4>; + apple,boost-bit = <2>; + }; + clpc: performance-controller@3 { compatible = "apple,t8132-clpc"; reg = <3>; diff --git a/drivers/soc/apple/t8132-platform-data.dts b/drivers/soc/apple/t8132-platform-data.dts index 3dd507182ecb4..52d97f73b81b3 100644 --- a/drivers/soc/apple/t8132-platform-data.dts +++ b/drivers/soc/apple/t8132-platform-data.dts @@ -37,6 +37,13 @@ apple,static-voters = <3>; }; +/* + * The PMC interface domains' parents are PMGR states, which a copied phandle + * cannot reach in the loader's tree, and no loader PMGR state uses them. + */ +/delete-node/ &pmc_dispext0; +/delete-node/ &pmc_dispext1; + &ps_gfx { status = "okay"; };