[RFC PATCH 1/6] x86/resctrl: Parse ACPI ERDT table and map RMDD domains by L3 cache ID
From: Chen Yu
Date: Wed May 27 2026 - 05:37:08 EST
From: Anil S Keshavamurthy <anil.s.keshavamurthy@xxxxxxxxx>
ERDT(Enhanced RDT) introduces a new top-level ACPI structure
(the ERDT) that the kernel must parse before any enhanced
RDT feature can be used. The ERDT improves the existing RDT
by switching low-level register access from MSR-based to
MMIO-based, which is more efficient.
The ERDT structure may include several sub ACPI tables:
- Resource Management Domain Description Structure (RMDD)
- CPU Agent Collection Description Structure (CACD)
- Cache Monitoring Registers for CPU Agents Description Structure
(CMRC)
There is one ERDT table per platform.
Each RMDD substructure in ERDT represents one resource management
domain (RMD), also known as an L3 domain. Thus, the total number
of RMDDs equals the number of L3 domains on the platform.
Each RMDD contains information such as MMIO addresses. This address
is used to retrieve RDT metrics like L3 occupancy.
Add basic ERDT ACPI table and sub-table parsing, and store the
relevant tables for later processing.
Among these sub-tables, RMDD requires special handling. There is one
RMDD per domain, and the domain ID reuses the L3 cache ID. Many code
paths need to retrieve an RMDD efficiently by domain ID (L3 cache ID).
Because L3 cache IDs are derived from x2APIC IDs and are not
contiguous, using a plain array indexed by domain ID would waste
memory. As a trade-off, an xarray is used to store these tables, with
the L3 cache ID as the key.
Suggested-by: Tony Luck <tony.luck@xxxxxxxxx>
Co-developed-by: Chen Yu <yu.c.chen@xxxxxxxxx>
Signed-off-by: Chen Yu <yu.c.chen@xxxxxxxxx>
Signed-off-by: Anil S Keshavamurthy <anil.s.keshavamurthy@xxxxxxxxx>
---
arch/x86/Kconfig | 4 +-
arch/x86/include/asm/resctrl.h | 2 +
arch/x86/kernel/cpu/resctrl/Makefile | 1 +
arch/x86/kernel/cpu/resctrl/core.c | 3 +
arch/x86/kernel/cpu/resctrl/erdt.c | 321 +++++++++++++++++++++++++
arch/x86/kernel/cpu/resctrl/internal.h | 3 +
6 files changed, 332 insertions(+), 2 deletions(-)
create mode 100644 arch/x86/kernel/cpu/resctrl/erdt.c
diff --git a/arch/x86/Kconfig b/arch/x86/Kconfig
index f3f7cb01d69d..97d210bd9bb5 100644
--- a/arch/x86/Kconfig
+++ b/arch/x86/Kconfig
@@ -515,7 +515,7 @@ config X86_MPPARSE
config X86_CPU_RESCTRL
bool "x86 CPU resource control support"
- depends on X86 && (CPU_SUP_INTEL || CPU_SUP_AMD)
+ depends on X86_64 && (CPU_SUP_INTEL || CPU_SUP_AMD)
depends on MISC_FILESYSTEMS
select ARCH_HAS_CPU_RESCTRL
select RESCTRL_FS
@@ -538,7 +538,7 @@ config X86_CPU_RESCTRL
config X86_CPU_RESCTRL_INTEL_AET
bool "Intel Application Energy Telemetry"
- depends on X86_64 && X86_CPU_RESCTRL && CPU_SUP_INTEL && INTEL_PMT_TELEMETRY=y && INTEL_TPMI=y
+ depends on X86_CPU_RESCTRL && CPU_SUP_INTEL && INTEL_PMT_TELEMETRY=y && INTEL_TPMI=y
help
Enable per-RMID telemetry events in resctrl.
diff --git a/arch/x86/include/asm/resctrl.h b/arch/x86/include/asm/resctrl.h
index 575f8408a9e7..97c2f6bc7a5f 100644
--- a/arch/x86/include/asm/resctrl.h
+++ b/arch/x86/include/asm/resctrl.h
@@ -40,6 +40,8 @@ struct resctrl_pqr_state {
u32 default_closid;
};
+bool erdt_enabled(void);
+
DECLARE_PER_CPU(struct resctrl_pqr_state, pqr_state);
extern bool rdt_alloc_capable;
diff --git a/arch/x86/kernel/cpu/resctrl/Makefile b/arch/x86/kernel/cpu/resctrl/Makefile
index 273ddfa30836..2216ee084832 100644
--- a/arch/x86/kernel/cpu/resctrl/Makefile
+++ b/arch/x86/kernel/cpu/resctrl/Makefile
@@ -2,6 +2,7 @@
obj-$(CONFIG_X86_CPU_RESCTRL) += core.o rdtgroup.o monitor.o
obj-$(CONFIG_X86_CPU_RESCTRL) += ctrlmondata.o
obj-$(CONFIG_X86_CPU_RESCTRL_INTEL_AET) += intel_aet.o
+obj-$(CONFIG_X86_CPU_RESCTRL) += erdt.o
obj-$(CONFIG_RESCTRL_FS_PSEUDO_LOCK) += pseudo_lock.o
# To allow define_trace.h's recursive include:
diff --git a/arch/x86/kernel/cpu/resctrl/core.c b/arch/x86/kernel/cpu/resctrl/core.c
index 7667cf7c4e94..893d2efe76f0 100644
--- a/arch/x86/kernel/cpu/resctrl/core.c
+++ b/arch/x86/kernel/cpu/resctrl/core.c
@@ -1012,6 +1012,7 @@ static __init void check_quirks(void)
static __init bool get_rdt_resources(void)
{
+ erdt_init();
rdt_alloc_capable = get_rdt_alloc_resources();
rdt_mon_capable = get_rdt_mon_resources();
@@ -1165,6 +1166,8 @@ static void __exit resctrl_arch_exit(void)
cpuhp_remove_state(rdt_online);
resctrl_exit();
+
+ erdt_exit();
}
__exitcall(resctrl_arch_exit);
diff --git a/arch/x86/kernel/cpu/resctrl/erdt.c b/arch/x86/kernel/cpu/resctrl/erdt.c
new file mode 100644
index 000000000000..2813f48f1411
--- /dev/null
+++ b/arch/x86/kernel/cpu/resctrl/erdt.c
@@ -0,0 +1,321 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Enhanced Resource Director Technology(ERDT)
+ *
+ * Copyright (C) 2026 Intel Corporation
+ *
+ */
+
+#define pr_fmt(fmt) "resctrl: " fmt
+
+#include <linux/cpu.h>
+#include <linux/err.h>
+#include <linux/xarray.h>
+#include <linux/resctrl.h>
+#include <linux/acpi.h>
+#include <asm/cpu.h>
+#include <asm/apic.h>
+#include <asm/cpu_device_id.h>
+#include "internal.h"
+
+enum erdt_mmio_type {
+ ERDT_MMIO_RMDD_CREG,
+ ERDT_MMIO_CMRC_BASE,
+ ERDT_MMIO_MAX
+};
+
+struct erdt_domain_info {
+ struct acpi_erdt_cacd *cacd;
+ struct acpi_erdt_cmrc *cmrc;
+ /* MMIO address */
+ void __iomem *base[ERDT_MMIO_MAX];
+};
+
+/* true if ERDT table is present and valid */
+static bool erdt_available;
+
+/* Global variable to hold ERDT ACPI table information for later processing */
+static DEFINE_XARRAY(erdt_domain_xa); /* Indexed by L3 cache ID */
+
+#define ERDT_VALID_VERSION 1
+
+static u32 valid_subtbl_mask;
+
+/*
+ * erdt_enabled - Check if the ERDT table is present and enabled
+ */
+bool erdt_enabled(void)
+{
+ return erdt_available;
+}
+
+/*
+ * lookup_logical_cpu_by_x2apicid - Map x2APIC ID to logical CPU number
+ */
+static __init int lookup_logical_cpu_by_x2apicid(u32 x2apicid)
+{
+ int cpu;
+
+ for_each_possible_cpu(cpu) {
+ if (cpu_physical_id(cpu) == x2apicid)
+ return cpu;
+ }
+
+ return -1;
+}
+
+/*
+ * get_l3_cache_id_from_cacd - Resolve L3 cache ID from CACD subtable
+ * @cacd: Pointer to the ACPI ERDT CACD structure
+ *
+ * Parses the X2APIC ID list in the given CACD subtable to
+ * identify an online logical CPU and uses it to query the associated
+ * L3 cache ID. The first valid CPU found is used for this lookup.
+ *
+ * The L3 cache ID is used as a unique domain key for ERDT domain
+ * registration and lookup.
+ *
+ * Return:
+ * L3 cache ID for the first matching CPU, or
+ * -1 if no valid CPU or L3 cache ID could be determined.
+ */
+static __init int get_l3_cache_id_from_cacd(struct acpi_erdt_cacd *cacd)
+{
+ int num_ids, cpu, online_cpu = -1, cache_id = -1, tmp;
+ struct cacheinfo *ci;
+
+ if (cacd->header.length < sizeof(*cacd) + sizeof(cacd->X2APICIDS[0])) {
+ pr_warn(FW_BUG "Invalid x2apicid CACD table\n");
+ return -1;
+ }
+
+ num_ids = (cacd->header.length - sizeof(*cacd)) / sizeof(cacd->X2APICIDS[0]);
+
+ guard(cpus_read_lock)();
+
+ for (int i = 0; i < num_ids; i++) {
+ cpu = lookup_logical_cpu_by_x2apicid(cacd->X2APICIDS[i]);
+ if (cpu == -1) {
+ pr_warn(FW_BUG "Unknown x2apicid 0x%x\n", cacd->X2APICIDS[i]);
+
+ return -1;
+ }
+
+ if (!cpu_online(cpu))
+ continue;
+
+ tmp = get_cpu_cacheinfo_id(cpu, RESCTRL_L3_CACHE);
+ if (tmp == -1) {
+ pr_warn(FW_BUG "Can not find L3 cache id for CPU%d\n", cpu);
+ return -1;
+ }
+
+ if (cache_id == -1)
+ cache_id = tmp;
+
+ if (tmp != cache_id) {
+ pr_warn(FW_BUG "CACD references multiple L3 cache instances\n");
+ return -1;
+ }
+ online_cpu = cpu;
+ }
+
+ if (online_cpu == -1)
+ return -1;
+
+ /*
+ * Check if CACD lists all CPUs in the LLC domain.
+ */
+ ci = get_cpu_cacheinfo_level(online_cpu, RESCTRL_L3_CACHE);
+ if (!ci || num_ids != cpumask_weight(&ci->shared_cpu_map)) {
+ pr_warn(FW_BUG "CACD does not list all the CPUs in L3 domain\n");
+ return -1;
+ }
+
+ return cache_id;
+}
+
+static void __iomem *erdt_ioremap_checked(phys_addr_t base, u32 size,
+ const char *desc)
+{
+ void __iomem *addr = ioremap(base, size << 12);
+
+ if (!addr)
+ pr_err("ERDT: Failed to map %s at phys addr %#llx (size: %u pages)\n",
+ desc, (unsigned long long)base, size);
+ return addr;
+}
+
+static void erdt_iounmap_domain(struct erdt_domain_info *domain)
+{
+ for (int i = 0; i < ERDT_MMIO_MAX; i++) {
+ if (domain->base[i]) {
+ iounmap(domain->base[i]);
+ domain->base[i] = NULL;
+ }
+ }
+}
+
+static void cleanup_one_domain(struct erdt_domain_info *d)
+{
+ erdt_iounmap_domain(d);
+ kfree(d);
+}
+
+static __init bool cacd_init(struct erdt_domain_info *d,
+ struct acpi_subtbl_hdr_16 *subtbl,
+ int *l3_cache_id)
+{
+ *l3_cache_id = get_l3_cache_id_from_cacd((struct acpi_erdt_cacd *)subtbl);
+
+ return *l3_cache_id != -1;
+}
+
+static __init bool parse_rmdd_entry(struct acpi_subtbl_hdr_16 *rmdd_hdr)
+{
+ struct acpi_erdt_rmdd *rmdd;
+ struct erdt_domain_info *domain_info;
+ struct acpi_subtbl_hdr_16 *subtbl;
+ int l3_cache_id = -1;
+ u32 subtbl_mask = 0;
+ void *rmdd_end;
+
+ if (rmdd_hdr->length < sizeof(*rmdd)) {
+ pr_info(FW_BUG "Invalid RMDD length %u\n", rmdd_hdr->length);
+ return false;
+ }
+
+ rmdd = (struct acpi_erdt_rmdd *)rmdd_hdr;
+
+ /* Quietly ignore non-CPU-based L3 domains (bit 0 set) */
+ if (!(rmdd->flags & 0x1))
+ return true;
+
+ domain_info = kzalloc(sizeof(*domain_info), GFP_KERNEL);
+ if (!domain_info)
+ return false;
+
+ domain_info->base[ERDT_MMIO_RMDD_CREG] = erdt_ioremap_checked(rmdd->creg_base, rmdd->creg_size,
+ "RMDD ctrl base");
+ if (!domain_info->base[ERDT_MMIO_RMDD_CREG])
+ goto cleanup;
+
+ rmdd_end = (void *)rmdd + rmdd->header.length;
+
+ /* Iterate through all sub-structures inside this RMDD block */
+ for (subtbl = (void *)rmdd + sizeof(*rmdd);
+ (void *)subtbl + sizeof(*subtbl) <= rmdd_end;
+ subtbl = (void *)subtbl + subtbl->length) {
+ if (subtbl->length < sizeof(*subtbl) ||
+ (void *)subtbl + subtbl->length > rmdd_end) {
+ pr_info("ERDT: Invalid subtable length in RMDD domain %d\n",
+ rmdd->domain_id);
+
+ goto cleanup;
+ }
+
+ switch (subtbl->type) {
+ case ACPI_ERDT_TYPE_CACD:
+ if (cacd_init(domain_info, subtbl, &l3_cache_id))
+ subtbl_mask |= BIT(ACPI_ERDT_TYPE_CACD);
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (l3_cache_id == -1) {
+ pr_info("ERDT: Failed to resolve L3 cache ID for RMDD domain %d\n",
+ rmdd->domain_id);
+
+ goto cleanup;
+ }
+
+ /* Require all RMDDs to support same set of sub-tables */
+ if (!valid_subtbl_mask) {
+ valid_subtbl_mask = subtbl_mask;
+ } else if (subtbl_mask != valid_subtbl_mask) {
+ pr_info(FW_BUG "Mismatch domain\n");
+ goto cleanup;
+ }
+
+ if (xa_insert(&erdt_domain_xa, l3_cache_id, domain_info, GFP_KERNEL)) {
+ pr_info("ERDT: Failed to store domain info for RMDD domain %d\n",
+ rmdd->domain_id);
+ goto cleanup;
+ }
+
+ return true;
+
+cleanup:
+ cleanup_one_domain(domain_info);
+ return false;
+}
+
+static void erdt_cleanup(void)
+{
+ struct erdt_domain_info *d;
+ unsigned long index;
+
+ xa_for_each(&erdt_domain_xa, index, d)
+ cleanup_one_domain(d);
+ xa_destroy(&erdt_domain_xa);
+}
+
+/*
+ * enumerate_erdt_table - Store pointer to ERDT and begin domain parsing
+ */
+static __init int enumerate_erdt_table(struct acpi_table_header *table_hdr)
+{
+ struct acpi_table_erdt *erdt = (struct acpi_table_erdt *)table_hdr;
+ struct acpi_subtbl_hdr_16 *subtbl;
+ void *table_end;
+
+ if (erdt->header.revision != ERDT_VALID_VERSION) {
+ pr_info("Unknown ERDT table revision %d\n", erdt->header.revision);
+ return -EINVAL;
+ }
+
+ if (erdt->header.length < sizeof(*erdt)) {
+ pr_info(FW_BUG "ERDT: Invalid table length %u\n", erdt->header.length);
+ return -EINVAL;
+ }
+
+ subtbl = (void *)erdt + sizeof(struct acpi_table_erdt);
+ table_end = (void *)erdt + erdt->header.length;
+
+ while ((void *)subtbl + sizeof(*subtbl) <= table_end) {
+ if (subtbl->length < sizeof(*subtbl) ||
+ (void *)subtbl + subtbl->length > table_end) {
+ pr_info("ERDT: Invalid subtable length\n");
+ goto cleanup;
+ }
+
+ if (subtbl->type == ACPI_ERDT_TYPE_RMDD)
+ if (!parse_rmdd_entry(subtbl))
+ goto cleanup;
+
+ subtbl = (void *)subtbl + subtbl->length;
+ }
+
+ erdt_available = true;
+
+ return 0;
+
+cleanup:
+ erdt_cleanup();
+ return -EINVAL;
+}
+
+/*
+ * erdt_init - ACPI ERDT table initialization entry point
+ */
+int __init erdt_init(void)
+{
+ return acpi_table_parse(ACPI_SIG_ERDT, enumerate_erdt_table);
+}
+
+void __exit erdt_exit(void)
+{
+ erdt_cleanup();
+}
diff --git a/arch/x86/kernel/cpu/resctrl/internal.h b/arch/x86/kernel/cpu/resctrl/internal.h
index e3cfa0c10e92..9c59bd5e028e 100644
--- a/arch/x86/kernel/cpu/resctrl/internal.h
+++ b/arch/x86/kernel/cpu/resctrl/internal.h
@@ -253,4 +253,7 @@ static inline void intel_aet_mon_domain_setup(int cpu, int id, struct rdt_resour
static inline bool intel_handle_aet_option(bool force_off, char *tok) { return false; }
#endif
+int erdt_init(void);
+void erdt_exit(void);
+
#endif /* _ASM_X86_RESCTRL_INTERNAL_H */
--
2.25.1