[PATCH 08/11] iommu/tegra-smmu: Defer display client translation to driver handover

From: Mikko Perttunen

Date: Mon Sep 14 2026 - 22:24:31 EST


Bootloaders may leave display controllers scanning out a boot splash
while the kernel boots. Enabling translation for them without special
care during that time would cause translation faults.

Because of this, tegra_smmu_def_domain_type has kept every device on an
identity domain at boot, and TegraDRM has parallel custom IOMMU domain
management code paths for these clients.

Mark the dc and dcb swgroups on Tegra30, Tegra114, Tegra124 and Tegra210
with a new defer_enable flag. tegra_smmu_enable() sets those swgroups up
as usual, ASID and per-client enables and all, but leaves
SMMU_ASID_ENABLE clear, so the clients stay in bypass. tegra_dc_probe()
then calls tegra_smmu_enable_translation() once it has asserted the
display controller's reset (and set up its final IOMMU domain), at which
point the hardware is guaranteed not to be fetching and translation can
be turned on.

If the display driver never probes, the clients simply stay in bypass,
which is what they do today.

Signed-off-by: Mikko Perttunen <mperttunen@xxxxxxxxxx>
---
drivers/gpu/drm/tegra/dc.c | 8 +++++++
drivers/iommu/tegra-smmu.c | 51 ++++++++++++++++++++++++++++++++++++++++-
drivers/memory/tegra/tegra114.c | 6 +++--
drivers/memory/tegra/tegra124.c | 6 +++--
drivers/memory/tegra/tegra210.c | 6 +++--
drivers/memory/tegra/tegra30.c | 6 +++--
include/soc/tegra/mc.h | 13 +++++++++++
7 files changed, 87 insertions(+), 9 deletions(-)

diff --git a/drivers/gpu/drm/tegra/dc.c b/drivers/gpu/drm/tegra/dc.c
index 0b3fcc7011b3..2ae4da07bbc9 100644
--- a/drivers/gpu/drm/tegra/dc.c
+++ b/drivers/gpu/drm/tegra/dc.c
@@ -19,6 +19,7 @@
#include <linux/reset.h>

#include <soc/tegra/common.h>
+#include <soc/tegra/mc.h>
#include <soc/tegra/pmc.h>

#include <drm/drm_atomic.h>
@@ -2751,6 +2752,13 @@ static int tegra_dc_init(struct host1x_client *client)
return err;
}

+ /*
+ * Enable translation now that the final domain is attached.
+ * The hardware was reset at probe and has not been programmed
+ * since.
+ */
+ tegra_smmu_enable_translation(client->dev);
+
if (dc->soc->wgrps)
primary = tegra_dc_add_shared_planes(drm, dc);
else
diff --git a/drivers/iommu/tegra-smmu.c b/drivers/iommu/tegra-smmu.c
index 67e7a7b925f0..6636a044905f 100644
--- a/drivers/iommu/tegra-smmu.c
+++ b/drivers/iommu/tegra-smmu.c
@@ -368,7 +368,13 @@ static void tegra_smmu_enable(struct tegra_smmu *smmu, unsigned int swgroup,
value = smmu_readl(smmu, group->reg);
value &= ~SMMU_ASID_MASK;
value |= SMMU_ASID_VALUE(asid);
- value |= SMMU_ASID_ENABLE;
+ /*
+ * A deferred client is set up completely, but with translation
+ * gated off, so it stays in bypass until its driver calls
+ * tegra_smmu_enable_translation().
+ */
+ if (!group->defer_enable)
+ value |= SMMU_ASID_ENABLE;
smmu_writel(smmu, value, group->reg);
} else {
pr_warn("%s group from swgroup %u not found\n", __func__,
@@ -417,6 +423,49 @@ static void tegra_smmu_disable(struct tegra_smmu *smmu, unsigned int swgroup,
}
}

+/**
+ * tegra_smmu_enable_translation - Release a client's swgroups to translation
+ * @dev: memory client device, already quiesced by its driver
+ *
+ * Enables translation for swgroups which were left in bypass at attach time
+ * because the client could still have been running from a pre-kernel
+ * configuration. Must be called after @dev has been attached to the domain it
+ * will use. Safe to call for any device; clients which were not deferred are
+ * unaffected.
+ */
+void tegra_smmu_enable_translation(struct device *dev)
+{
+ struct iommu_fwspec *fwspec = dev_iommu_fwspec_get(dev);
+ struct tegra_smmu *smmu = dev_iommu_priv_get(dev);
+ struct iommu_domain *domain;
+ unsigned int i;
+
+ if (!fwspec || !smmu)
+ return;
+
+ domain = iommu_get_domain_for_dev(dev);
+ if (!domain || !(domain->type & __IOMMU_DOMAIN_PAGING))
+ return;
+
+ mutex_lock(&smmu->lock);
+
+ for (i = 0; i < fwspec->num_ids; i++) {
+ const struct tegra_smmu_swgroup *group;
+ u32 value;
+
+ group = tegra_smmu_find_swgroup(smmu, fwspec->ids[i]);
+ if (!group || !group->defer_enable)
+ continue;
+
+ value = smmu_readl(smmu, group->reg);
+ value |= SMMU_ASID_ENABLE;
+ smmu_writel(smmu, value, group->reg);
+ }
+
+ mutex_unlock(&smmu->lock);
+}
+EXPORT_SYMBOL_GPL(tegra_smmu_enable_translation);
+
static int tegra_smmu_as_prepare(struct tegra_smmu *smmu,
struct tegra_smmu_as *as)
{
diff --git a/drivers/memory/tegra/tegra114.c b/drivers/memory/tegra/tegra114.c
index 2cb153091228..2883c4f91144 100644
--- a/drivers/memory/tegra/tegra114.c
+++ b/drivers/memory/tegra/tegra114.c
@@ -1028,8 +1028,10 @@ static const struct tegra_mc_client tegra114_mc_clients[] = {
};

static const struct tegra_smmu_swgroup tegra114_swgroups[] = {
- { .name = "dc", .swgroup = TEGRA_SWGROUP_DC, .reg = 0x240 },
- { .name = "dcb", .swgroup = TEGRA_SWGROUP_DCB, .reg = 0x244 },
+ { .name = "dc", .swgroup = TEGRA_SWGROUP_DC, .reg = 0x240,
+ .defer_enable = true },
+ { .name = "dcb", .swgroup = TEGRA_SWGROUP_DCB, .reg = 0x244,
+ .defer_enable = true },
{ .name = "epp", .swgroup = TEGRA_SWGROUP_EPP, .reg = 0x248 },
{ .name = "g2", .swgroup = TEGRA_SWGROUP_G2, .reg = 0x24c },
{ .name = "avpc", .swgroup = TEGRA_SWGROUP_AVPC, .reg = 0x23c },
diff --git a/drivers/memory/tegra/tegra124.c b/drivers/memory/tegra/tegra124.c
index df87c5038625..a38fbf2baf6c 100644
--- a/drivers/memory/tegra/tegra124.c
+++ b/drivers/memory/tegra/tegra124.c
@@ -1070,8 +1070,10 @@ static const struct tegra_mc_client tegra124_mc_clients[] = {
};

static const struct tegra_smmu_swgroup tegra124_swgroups[] = {
- { .name = "dc", .swgroup = TEGRA_SWGROUP_DC, .reg = 0x240 },
- { .name = "dcb", .swgroup = TEGRA_SWGROUP_DCB, .reg = 0x244 },
+ { .name = "dc", .swgroup = TEGRA_SWGROUP_DC, .reg = 0x240,
+ .defer_enable = true },
+ { .name = "dcb", .swgroup = TEGRA_SWGROUP_DCB, .reg = 0x244,
+ .defer_enable = true },
{ .name = "afi", .swgroup = TEGRA_SWGROUP_AFI, .reg = 0x238 },
{ .name = "avpc", .swgroup = TEGRA_SWGROUP_AVPC, .reg = 0x23c },
{ .name = "hda", .swgroup = TEGRA_SWGROUP_HDA, .reg = 0x254 },
diff --git a/drivers/memory/tegra/tegra210.c b/drivers/memory/tegra/tegra210.c
index f58f3ef6f681..92e809cd7c26 100644
--- a/drivers/memory/tegra/tegra210.c
+++ b/drivers/memory/tegra/tegra210.c
@@ -1166,8 +1166,10 @@ static const struct tegra_mc_client tegra210_mc_clients[] = {
static const struct tegra_smmu_swgroup tegra210_swgroups[] = {
{ .name = "afi", .swgroup = TEGRA_SWGROUP_AFI, .reg = 0x238 },
{ .name = "avpc", .swgroup = TEGRA_SWGROUP_AVPC, .reg = 0x23c },
- { .name = "dc", .swgroup = TEGRA_SWGROUP_DC, .reg = 0x240 },
- { .name = "dcb", .swgroup = TEGRA_SWGROUP_DCB, .reg = 0x244 },
+ { .name = "dc", .swgroup = TEGRA_SWGROUP_DC, .reg = 0x240,
+ .defer_enable = true },
+ { .name = "dcb", .swgroup = TEGRA_SWGROUP_DCB, .reg = 0x244,
+ .defer_enable = true },
{ .name = "hc", .swgroup = TEGRA_SWGROUP_HC, .reg = 0x250 },
{ .name = "hda", .swgroup = TEGRA_SWGROUP_HDA, .reg = 0x254 },
{ .name = "isp2", .swgroup = TEGRA_SWGROUP_ISP2, .reg = 0x258 },
diff --git a/drivers/memory/tegra/tegra30.c b/drivers/memory/tegra/tegra30.c
index 8389e3af0121..0ee4715413ba 100644
--- a/drivers/memory/tegra/tegra30.c
+++ b/drivers/memory/tegra/tegra30.c
@@ -1139,8 +1139,10 @@ static const struct tegra_mc_client tegra30_mc_clients[] = {
};

static const struct tegra_smmu_swgroup tegra30_swgroups[] = {
- { .name = "dc", .swgroup = TEGRA_SWGROUP_DC, .reg = 0x240 },
- { .name = "dcb", .swgroup = TEGRA_SWGROUP_DCB, .reg = 0x244 },
+ { .name = "dc", .swgroup = TEGRA_SWGROUP_DC, .reg = 0x240,
+ .defer_enable = true },
+ { .name = "dcb", .swgroup = TEGRA_SWGROUP_DCB, .reg = 0x244,
+ .defer_enable = true },
{ .name = "epp", .swgroup = TEGRA_SWGROUP_EPP, .reg = 0x248 },
{ .name = "g2", .swgroup = TEGRA_SWGROUP_G2, .reg = 0x24c },
{ .name = "mpe", .swgroup = TEGRA_SWGROUP_MPE, .reg = 0x264 },
diff --git a/include/soc/tegra/mc.h b/include/soc/tegra/mc.h
index 25d465d70493..ea67972fd585 100644
--- a/include/soc/tegra/mc.h
+++ b/include/soc/tegra/mc.h
@@ -70,6 +70,14 @@ struct tegra_smmu_swgroup {
const char *name;
unsigned int swgroup;
unsigned int reg;
+
+ /*
+ * Set for clients which may already be running when the kernel takes
+ * over, e.g. display controllers scanning out a boot splash. They are
+ * left in bypass until their driver calls
+ * tegra_smmu_enable_translation().
+ */
+ bool defer_enable;
};

struct tegra_smmu_group_soc {
@@ -103,6 +111,7 @@ struct tegra_smmu *tegra_smmu_probe(struct device *dev,
const struct tegra_smmu_soc *soc,
struct tegra_mc *mc);
void tegra_smmu_remove(struct tegra_smmu *smmu);
+void tegra_smmu_enable_translation(struct device *dev);
#else
static inline struct tegra_smmu *
tegra_smmu_probe(struct device *dev, const struct tegra_smmu_soc *soc,
@@ -114,6 +123,10 @@ tegra_smmu_probe(struct device *dev, const struct tegra_smmu_soc *soc,
static inline void tegra_smmu_remove(struct tegra_smmu *smmu)
{
}
+
+static inline void tegra_smmu_enable_translation(struct device *dev)
+{
+}
#endif

struct tegra_mc_reset {

--
2.55.0