[PATCH v5 1/4] lib: Add generic platform filtering framework

<[email protected]> Wed, 5 Aug 2026 16:28:45 -0400
Newsgroups org.freedesktop.lists.igt-dev
Message-ID <[email protected]>
From: Vitaly Prosyak <[email protected]>

Implement vendor-agnostic platform filtering system that allows any
vendor to plug in platform-specific test skipping logic via callbacks.

Key design elements:
- struct platform_filter_ops: vendor callback interface
- struct platform_skip_entry: vendor-neutral skip rule representation
- enum skip_source: three-tier priority (built-in, config, env)
- API functions: init, should_skip, require, dump

Platform filtering is automatic - tests only need to call
platform_filter_init() once in igt_fixture. The IGT framework
automatically checks each subtest before execution via __igt_run_subtest().

Example usage:
  igt_fixture {
      vendor_platform_filter_init(platform_info);
  }

  igt_subtest("test") {
      // Automatic filtering - no manual call needed!
      test_code();
  }

Cc: Kamil Konieczny <[email protected]>
Cc: Jani Nikula <[email protected]>
Cc: Jesse Zhang <[email protected]>
Cc: Christian König <[email protected]>
Cc: Alex Deucher <[email protected]>
Cc: Krzysztof Karas <[email protected]>

v5 changes (addressing Kamil Konieczny's review):
   -This combines both the interface (header) and implementation in a
   -single commit as requested by Kamil Konieczny.
   -**IMPORTANT**: This commit is generic and vendor-agnostic, containing
   -NO AMD-specific implementation. The AMD backend is in a separate commit
   -(patch 2/6 "lib/amdgpu: Add AMD platform filtering backend").
   - Reorganize and improve commit content.
   - Squash patch 3: Add platform filter initialization check to fix
     incremental compilation.

v4 changes (addressing Krzysztof Karas detailed review of v3):
 - Fixed documentation comment placement - ALL moved before functions
 - Reorganized commit message for clarity (v4 changes first)
 - Added prominent note that this is generic (no vendor code)
 - Proper [PATCH v4 X/Y] format using git format-patch --subject-prefix

v3 changes (addressing Kamil Konieczny's review):
 - Squashed header and implementation into single commit
 - Removed unnecessary comment "Function prototypes..." from header
 - Comprehensive documentation in separate commit
   (docs/platform_filtering.md)
 - Automatic filtering via __igt_run_subtest() hook (no manual require
   calls)
 - Fixed code style: include order, SPDX style, checkpatch clean
 - Removed unnecessary newlines before single-statement returns

Design rationale (addresses feedback from original RFC discussion):

This implementation addresses feedback from multiple reviewers on the
original RFC patch series.

1. Jani Nikula requested vendor-agnostic design:
   "I would have expected an attempt to make an IGT shared filtering
   system generic enough to plug into any vendor's platforms."

   Resolution: Implemented vendor-agnostic callback-based design via
   platform_filter_ops structure. Any vendor (Intel, AMD, Qualcomm, etc.)
   can provide their own backend without modifying core framework.

2. Kamil Konieczny requested config file support:
   a) "Add also example with config file as env vars are not convenient
      for large tests lists"

      Resolution: Comprehensive documentation added in patch 5/6
      (docs/platform_filtering.md) showing config file as RECOMMENDED
      method with real-world examples, wildcards, and best practices.

   b) "imho you can get test name in require, no need to repeat it"

      Resolution: Went further - v3 removes igt_platform_require()
      entirely. Filtering is now automatic via __igt_run_subtest() hook
      in patch 4/6. Zero manual calls needed in subtests.

   c) Code style (include order, alignment, igt_debug vs igt_info)

      Resolution: Fixed in v3. Includes alphabetically ordered, SPDX
      headers use // style, checkpatch clean.

   d) "No need for this comment" (function prototypes comment)

      Resolution: Removed unnecessary comment from header file.

   e) "Please squash this into second patch"

      Resolution: Header and implementation now in single commit.

3. Multi-GPU support (integrated + discrete):
   Current design queries platform once in igt_fixture. For multi-GPU
   scenarios, tests can call platform_filter_init() per-device with
   device-specific platform_info.

Signed-off-by: Vitaly Prosyak <[email protected]>
Change-Id: I389d8a4e5bd67cdd00ac625c64b24e4d9b092e17
---
 lib/igt_platform_filter.c | 574 ++++++++++++++++++++++++++++++++++++++
 lib/igt_platform_filter.h | 124 ++++++++
 lib/meson.build           |   1 +
 3 files changed, 699 insertions(+)
 create mode 100644 lib/igt_platform_filter.c
 create mode 100644 lib/igt_platform_filter.h

diff --git a/lib/igt_platform_filter.c b/lib/igt_platform_filter.c
new file mode 100644
index 000000000..0b9452c40
--- /dev/null
+++ b/lib/igt_platform_filter.c
@@ -0,0 +1,574 @@
+// SPDX-License-Identifier: MIT
+// Copyright 2026 Advanced Micro Devices, Inc.
+/*
+ * Generic platform-based test filtering framework
+ *
+ * This is a vendor-agnostic filtering system. Vendor-specific logic
+ * is implemented via platform_filter_ops callbacks.
+ */
+
+#include <ctype.h>
+#include <fnmatch.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "igt.h"
+#include "igt_platform_filter.h"
+
+/* Maximum entries from config file and env variable */
+#define MAX_CONFIG_ENTRIES 256
+#define MAX_ENV_ENTRIES 128
+#define MAX_LINE_LENGTH 512
+
+/* Filter context - holds all runtime state (no globals) */
+struct platform_filter_context {
+	const struct platform_filter_ops *ops;
+	const void *platform_info;
+
+	struct platform_skip_entry config_entries[MAX_CONFIG_ENTRIES];
+	int config_entry_count;
+
+	struct platform_skip_entry env_entries[MAX_ENV_ENTRIES];
+	int env_entry_count;
+
+	bool initialized;
+	char current_platform_name[64];
+};
+
+/* Single static instance - initialized on first use */
+static struct platform_filter_context *get_filter_context(void)
+{
+	static struct platform_filter_context ctx = {0};
+
+	return &ctx;
+}
+
+/*
+ * ================================================================
+ * HELPER FUNCTIONS
+ * ================================================================ */
+
+/* Helper: Trim whitespace from string */
+static char *trim(char *str)
+{
+	char *end;
+
+	while (isspace(*str))
+		str++;
+
+	if (*str == 0)
+		return str;
+
+	end = str + strlen(str) - 1;
+	while (end > str && isspace(*end))
+		end--;
+
+	*(end + 1) = 0;
+	return str;
+}
+
+/* Helper: Match wildcard or exact string */
+static bool match_string(const char *pattern, const char *str)
+{
+	if (!pattern || !str)
+		return false;
+
+	if (strcmp(pattern, "*") == 0)
+		return true;
+	return fnmatch(pattern, str, 0) == 0;
+}
+
+/* Helper: Check if entry matches platform/test/subtest */
+static bool entry_matches(const struct platform_filter_context *ctx,
+			  const struct platform_skip_entry *entry,
+			  const char *test_name,
+			  const char *subtest_name)
+{
+	if (entry->platform_data && ctx->ops->match_platform) {
+		if (!ctx->ops->match_platform(ctx->platform_info, entry->platform_data))
+			return false;
+	}
+
+	if (entry->test_name && strcmp(entry->test_name, "*") != 0) {
+		if (!test_name || !match_string(entry->test_name, test_name))
+			return false;
+	}
+
+	if (entry->subtest_glob && strcmp(entry->subtest_glob, "*") != 0) {
+		if (!subtest_name || !match_string(entry->subtest_glob, subtest_name))
+			return false;
+	}
+	return true;
+}
+
+/*
+ * ================================================================
+ * CONFIG FILE PARSER (/etc/igt/platform_skip.conf)
+ * ================================================================ */
+
+/* Parse one line from config file */
+static bool parse_config_line(struct platform_filter_context *ctx,
+			      char *line,
+			       struct platform_skip_entry *entry)
+{
+	char *platform, *test, *subtest, *reason;
+
+	/* Skip comments and empty lines */
+	line = trim(line);
+	if (line[0] == '#' || line[0] == 0)
+		return false;
+
+	/* Format: platform:test:subtest:reason */
+	platform = strtok(line, ":");
+	test = strtok(NULL, ":");
+	subtest = strtok(NULL, ":");
+	reason = strtok(NULL, "\n");
+
+	if (!platform || !test || !subtest) {
+		igt_warn("Invalid config line format (expected platform:test:subtest:reason)\n");
+		return false;
+	}
+
+	/* Allocate and copy strings */
+	entry->test_name = strdup(trim(test));
+	entry->subtest_glob = strdup(trim(subtest));
+	entry->reason = reason ? strdup(trim(reason)) : strdup("No reason");
+
+	/* Parse platform using vendor callback */
+	platform = trim(platform);
+	if (ctx->ops->parse_platform_config && strcmp(platform, "*") != 0) {
+		/* Vendor-specific platform string */
+		if (!ctx->ops->parse_platform_config(platform, &entry->platform_data)) {
+			igt_warn("Failed to parse platform: %s\n", platform);
+			free((void *)entry->test_name);
+			free((void *)entry->subtest_glob);
+			free((void *)entry->reason);
+			return false;
+		}
+	} else {
+		/* Wildcard or no vendor-specific parsing available */
+		entry->platform_data = NULL;
+	}
+	return true;
+}
+
+/* Load config file */
+static void load_config_file(struct platform_filter_context *ctx, const char *filename)
+{
+	FILE *f;
+	char line[MAX_LINE_LENGTH];
+
+	f = fopen(filename, "r");
+	if (!f) {
+		igt_debug("Config file not found: %s\n", filename);
+
+		return;
+	}
+
+	igt_info("Loading platform skip config from: %s\n", filename);
+
+	while (fgets(line, sizeof(line), f)) {
+		if (ctx->config_entry_count >= MAX_CONFIG_ENTRIES) {
+			igt_warn("Config file has too many entries (max %d)\n",
+				 MAX_CONFIG_ENTRIES);
+			break;
+		}
+
+		if (parse_config_line(ctx, line, &ctx->config_entries[ctx->config_entry_count])) {
+			ctx->config_entry_count++;
+		}
+	}
+
+	fclose(f);
+	igt_info("Loaded %d skip rules from config file\n", ctx->config_entry_count);
+}
+
+/*
+ * ================================================================
+ * ENVIRONMENT VARIABLE PARSER (IGT_PLATFORM_SKIP_CONFIG)
+ * ================================================================ */
+
+/* Parse environment variable entries (semicolon-separated) */
+static void load_env_variable(struct platform_filter_context *ctx)
+{
+	char *env, *env_copy, *entry_str, *saveptr;
+	const char *env_value;
+
+	env_value = getenv("IGT_PLATFORM_SKIP_CONFIG");
+	if (!env_value || env_value[0] == 0) {
+		igt_debug("IGT_PLATFORM_SKIP_CONFIG not set\n");
+
+		return;
+	}
+
+	igt_info("Loading platform skip config from IGT_PLATFORM_SKIP_CONFIG\n");
+
+	env_copy = strdup(env_value);
+	env = env_copy;
+
+	/* Parse semicolon-separated entries */
+	while ((entry_str = strtok_r(env, ";", &saveptr)) != NULL) {
+		env = NULL; /* For subsequent strtok_r calls */
+
+		if (ctx->env_entry_count >= MAX_ENV_ENTRIES) {
+			igt_warn("Too many env variable entries (max %d)\n",
+				 MAX_ENV_ENTRIES);
+			break;
+		}
+
+		if (parse_config_line(ctx, entry_str, &ctx->env_entries[ctx->env_entry_count])) {
+			ctx->env_entry_count++;
+		}
+	}
+
+	free(env_copy);
+	igt_info("Loaded %d skip rules from environment variable\n", ctx->env_entry_count);
+}
+
+/*
+ * ================================================================
+ * PUBLIC API IMPLEMENTATION
+ * ================================================================ */
+
+/**
+ * igt_platform_filter_init:
+ * @ops: Platform-specific operation callbacks
+ * @platform_info: Vendor-specific platform identification data
+ *
+ * Initialize the platform filtering system with vendor-specific backend.
+ * This sets up the filter context and loads skip rules from three sources
+ * in priority order:
+ * 1. Built-in rules (highest priority, vendor-provided)
+ * 2. Config file /etc/igt/platform_skip.conf
+ * 3. Environment variable IGT_PLATFORM_SKIP_CONFIG (lowest priority)
+ *
+ * The filtering system is vendor-agnostic. Platform matching logic is
+ * provided through the @ops callbacks, allowing each vendor to implement
+ * their own identification scheme (e.g., AMD uses family_id/chip_rev,
+ * Intel could use platform_id/stepping).
+ *
+ * Must be called once before using igt_platform_require().
+ */
+void igt_platform_filter_init(const struct platform_filter_ops *ops,
+			      const void *platform_info)
+{
+	struct platform_filter_context *ctx = get_filter_context();
+
+	if (ctx->initialized)
+
+		return;
+
+	if (!ops) {
+		igt_warn("Platform filter ops is NULL, filtering disabled\n");
+
+		return;
+	}
+
+	ctx->ops = ops;
+	ctx->platform_info = platform_info;
+
+	igt_info("Initializing platform filter system (3-tier priority) for vendor: %s\n",
+		 ops->name ? ops->name : "unknown");
+
+	/* Get current platform name */
+
+	if (ops->get_platform_name) {
+		const char *pname = ops->get_platform_name(platform_info);
+
+		snprintf(ctx->current_platform_name, sizeof(ctx->current_platform_name),
+			 "%s", pname ? pname : "unknown");
+	}
+
+	/* Priority 1: Built-in array (vendor-specific) */
+	igt_debug("  Priority 1: Built-in array (vendor-specific)\n");
+
+	/* Priority 2: Config file */
+	igt_debug("  Priority 2: Config file /etc/igt/platform_skip.conf\n");
+	load_config_file(ctx, "/etc/igt/platform_skip.conf");
+
+	/* Priority 3: Environment variable */
+	igt_debug("  Priority 3: Environment variable IGT_PLATFORM_SKIP_CONFIG\n");
+	load_env_variable(ctx);
+
+	ctx->initialized = true;
+	igt_info("Platform filter initialization complete\n");
+}
+
+/**
+ * igt_platform_should_skip:
+ * @test_name: Name of the test
+ * @subtest_name: Name of the subtest (or NULL for test-level check)
+ *
+ * Check if a test/subtest should be skipped based on platform filtering rules.
+ *
+ * Returns: true if the test should be skipped, false otherwise
+ */
+bool igt_platform_should_skip(const char *test_name,
+			      const char *subtest_name,
+			       enum skip_source *source,
+			       const char **reason)
+{
+	struct platform_filter_context *ctx = get_filter_context();
+	const struct platform_skip_entry *entry;
+	int i, count;
+
+	if (!ctx->initialized) {
+		igt_warn("Platform filter not initialized\n");
+		if (source)
+			*source = SKIP_SOURCE_NONE;
+		if (reason)
+			*reason = NULL;
+		return false;
+	}
+
+	/* Priority 1: Check built-in array FIRST */
+	if (ctx->ops->get_builtin_rules) {
+		const struct platform_skip_entry *builtin = ctx->ops->get_builtin_rules(&count);
+
+		for (i = 0; i < count; i++) {
+			entry = &builtin[i];
+			if (entry_matches(ctx, entry, test_name, subtest_name)) {
+				if (source)
+					*source = SKIP_SOURCE_BUILTIN;
+				if (reason)
+					*reason = entry->reason;
+				igt_debug("Skip (built-in): %s:%s - %s\n",
+					  entry->test_name ? entry->test_name : "*",
+					  entry->subtest_glob ? entry->subtest_glob : "*",
+					  entry->reason ? entry->reason : "no reason");
+				return true;
+			}
+		}
+	}
+
+	/* Priority 2: Check config file */
+	for (i = 0; i < ctx->config_entry_count; i++) {
+		entry = &ctx->config_entries[i];
+		if (entry_matches(ctx, entry, test_name, subtest_name)) {
+			if (source)
+				*source = SKIP_SOURCE_CONFIG;
+			if (reason)
+				*reason = entry->reason;
+			igt_debug("Skip (config): %s:%s - %s\n",
+				  entry->test_name, entry->subtest_glob, entry->reason);
+			return true;
+		}
+	}
+
+	/* Priority 3: Check environment variable */
+	for (i = 0; i < ctx->env_entry_count; i++) {
+		entry = &ctx->env_entries[i];
+		if (entry_matches(ctx, entry, test_name, subtest_name)) {
+			if (source)
+				*source = SKIP_SOURCE_ENV;
+			if (reason)
+				*reason = entry->reason;
+			igt_debug("Skip (env): %s:%s - %s\n",
+				  entry->test_name, entry->subtest_glob, entry->reason);
+			return true;
+		}
+	}
+
+	if (source)
+		*source = SKIP_SOURCE_NONE;
+	if (reason)
+		*reason = NULL;
+	return false;
+}
+
+/**
+ * igt_platform_require:
+ * @subtest_name: Name of the subtest to check
+ *
+ * Check if current subtest should be skipped and call igt_skip() if matched.
+ * This integrates platform filtering with IGT's standard skip mechanism.
+ *
+ * The function automatically determines the test name from igt_test_name().
+ * If a skip rule matches, calls igt_skip() with the configured reason.
+ */
+void igt_platform_require(const char *subtest_name)
+{
+	enum skip_source source;
+	const char *test_name = igt_test_name();
+	const char *reason;
+
+	if (igt_platform_should_skip(test_name, subtest_name, &source, &reason)) {
+		const char *source_str;
+
+		switch (source) {
+		case SKIP_SOURCE_BUILTIN:
+			source_str = "built-in array";
+			break;
+		case SKIP_SOURCE_CONFIG:
+			source_str = "config file";
+			break;
+		case SKIP_SOURCE_ENV:
+			source_str = "environment variable";
+			break;
+		default:
+			source_str = "unknown";
+		}
+
+		igt_skip("Skipped on this platform [%s]: %s\n",
+			 source_str, reason ? reason : "no reason");
+	}
+}
+
+/**
+ * igt_platform_filter_dump:
+ *
+ * Dump the current platform filtering configuration to stdout.
+ * Shows all loaded skip rules from built-in, config file, and environment
+ * variable sources. Useful for debugging which rules are active.
+ */
+void igt_platform_filter_dump(void)
+{
+	struct platform_filter_context *ctx = get_filter_context();
+	const struct platform_skip_entry *entry;
+	int i, total_count, count;
+
+	if (!ctx->initialized) {
+		igt_info("Platform filter not initialized\n");
+
+		return;
+	}
+
+	igt_info("\n");
+	igt_info("═══════════════════════════════════════════════════════════════════\n");
+	igt_info(" PLATFORM SKIP FILTER CONFIGURATION - THREE-TIER PRIORITY SYSTEM\n");
+	igt_info("═══════════════════════════════════════════════════════════════════\n\n");
+
+	igt_info("Vendor: %s\n", ctx->ops->name ? ctx->ops->name : "unknown");
+	if (ctx->current_platform_name[0]) {
+		igt_info("Current Platform: %s\n\n", ctx->current_platform_name);
+	}
+
+	/* Priority 1: Built-in array */
+	igt_info("───────────────────────────────────────────────────────────────────\n");
+	igt_info(" PRIORITY 1: BUILT-IN PRODUCTION ARRAY (VENDOR-SPECIFIC)\n");
+	igt_info(" Source: Vendor implementation\n");
+	igt_info("───────────────────────────────────────────────────────────────────\n");
+
+	total_count = 0;
+	if (ctx->ops->get_builtin_rules) {
+		const struct platform_skip_entry *builtin = ctx->ops->get_builtin_rules(&count);
+
+		for (i = 0; i < count; i++) {
+			entry = &builtin[i];
+			total_count++;
+			igt_info("  %2d. %s : %s\n",
+				 total_count,
+			       entry->test_name ? entry->test_name : "*",
+			       entry->subtest_glob ? entry->subtest_glob : "*");
+			igt_info("      Reason: %s\n", entry->reason ? entry->reason : "no reason");
+
+			/* Print platform data if vendor provides dump callback */
+			if (entry->platform_data && ctx->ops->dump_platform_data) {
+				igt_info("      Platform: ");
+				ctx->ops->dump_platform_data(entry->platform_data);
+				igt_info("\n");
+			}
+			igt_info("\n");
+		}
+	}
+	if (total_count == 0) {
+		igt_info("  (No built-in rules)\n\n");
+	}
+
+	/* Priority 2: Config file */
+	igt_info("───────────────────────────────────────────────────────────────────\n");
+	igt_info(" PRIORITY 2: DEVELOPMENT CONFIG FILE\n");
+	igt_info(" Source: /etc/igt/platform_skip.conf\n");
+	igt_info("───────────────────────────────────────────────────────────────────\n");
+
+	if (ctx->config_entry_count > 0) {
+		for (i = 0; i < ctx->config_entry_count; i++) {
+			entry = &ctx->config_entries[i];
+			igt_info("  %2d. %s : %s\n",
+				 i + 1,
+			       entry->test_name,
+			       entry->subtest_glob);
+			igt_info("      Reason: %s\n\n", entry->reason);
+		}
+	} else {
+		igt_info("  (No config file rules loaded)\n\n");
+	}
+
+	/* Priority 3: Environment variable */
+	igt_info("───────────────────────────────────────────────────────────────────\n");
+	igt_info(" PRIORITY 3: RUNTIME ENVIRONMENT VARIABLE\n");
+	igt_info(" Source: IGT_PLATFORM_SKIP_CONFIG\n");
+	igt_info("───────────────────────────────────────────────────────────────────\n");
+
+	if (ctx->env_entry_count > 0) {
+		for (i = 0; i < ctx->env_entry_count; i++) {
+			entry = &ctx->env_entries[i];
+
+			igt_info("  %2d. %s : %s\n",
+				 i + 1,
+			       entry->test_name,
+			       entry->subtest_glob);
+			igt_info("      Reason: %s\n\n", entry->reason);
+		}
+	} else {
+		igt_info("  (No environment variable rules)\n\n");
+	}
+
+	igt_info("───────────────────────────────────────────────────────────────────\n");
+	igt_info(" SUMMARY\n");
+	igt_info("───────────────────────────────────────────────────────────────────\n");
+	igt_info("  Built-in rules:     %d\n", total_count);
+	igt_info("  Config file rules:  %d\n", ctx->config_entry_count);
+	igt_info("  Environment rules:  %d\n", ctx->env_entry_count);
+	igt_info("  Total skip rules:   %d\n", total_count + ctx->config_entry_count + ctx->env_entry_count);
+	igt_info("\n");
+	igt_info("═══════════════════════════════════════════════════════════════════\n\n");
+}
+
+/**
+ * igt_platform_filter_dump_to_file:
+ * @filename: Path to output file
+ *
+ * Dump platform filtering configuration to a file.
+ *
+ * Returns: 0 on success, -1 on error
+ */
+int igt_platform_filter_dump_to_file(const char *filename)
+{
+	FILE *old_stdout;
+	FILE *f;
+
+	f = fopen(filename, "w");
+	if (!f) {
+		igt_warn("Failed to open %s for writing\n", filename);
+		return -1;
+	}
+
+	/* Redirect stdout to file */
+	old_stdout = stdout;
+	stdout = f;
+
+	igt_platform_filter_dump();
+
+	/* Restore stdout */
+	stdout = old_stdout;
+	fclose(f);
+
+	igt_info("Platform filter configuration dumped to: %s\n", filename);
+	return 0;
+}
+
+/**
+ * igt_platform_filter_is_initialized:
+ *
+ * Check if platform filtering has been initialized.
+ *
+ * Returns: true if initialized, false otherwise
+ */
+bool igt_platform_filter_is_initialized(void)
+{
+	struct platform_filter_context *ctx = get_filter_context();
+
+	return ctx && ctx->initialized;
+}
diff --git a/lib/igt_platform_filter.h b/lib/igt_platform_filter.h
new file mode 100644
index 000000000..b83440514
--- /dev/null
+++ b/lib/igt_platform_filter.h
@@ -0,0 +1,124 @@
+/* SPDX-License-Identifier: MIT
+ * Copyright 2026 Advanced Micro Devices, Inc.
+ */
+
+#ifndef IGT_PLATFORM_FILTER_H
+#define IGT_PLATFORM_FILTER_H
+
+#include <stdbool.h>
+
+/**
+ * SECTION: igt_platform_filter
+ * @short_description: Generic platform-based test filtering framework
+ * @title: Platform Filter
+ * @include: igt_platform_filter.h
+ *
+ * Generic test filtering system that allows skipping tests/subtests based
+ * on platform characteristics. Designed to be vendor-agnostic with
+ * vendor-specific backends.
+ *
+ * Three-tier priority system (checked in sequence, first match wins):
+ *   1. PRODUCTION: Built-in compile-time rules (vendor-specific)
+ *   2. DEVELOPMENT: Config file /etc/igt/platform_skip.conf
+ *   3. RUNTIME: Environment variable IGT_PLATFORM_SKIP_CONFIG
+ *
+ * Vendor Implementation:
+ *   Each vendor implements platform_filter_ops callbacks to provide:
+ *   - Platform identification and matching logic
+ *   - Platform-specific data structures
+ *   - Built-in skip rules
+ *
+ * Usage in tests:
+ *   igt_fixture() {
+ *       igt_platform_filter_init(vendor_ops, platform_info);
+ *   }
+ *
+ *   igt_subtest("my-test") {
+ *       // Automatic filtering - no manual call needed!
+ *       test_code();
+ *   }
+ *
+ * Config file format (/etc/igt/platform_skip.conf):
+ *   # Lines starting with # are comments
+ *   # Format: platform:test:subtest:reason
+ *   # Use * as wildcard
+ *
+ *   navi48:*:*:All tests disabled on Navi48
+ *   alderlake:i915_pm:*:Power management tests broken
+ *
+ * Environment variable format (IGT_PLATFORM_SKIP_CONFIG):
+ *   Same as config file, semicolon-separated entries:
+ *   export IGT_PLATFORM_SKIP_CONFIG="navi48:*:*:Testing;navi10:amd_basic:*:Broken"
+ */
+
+/* Maximum platform ranges per skip entry */
+#define MAX_PLATFORM_RANGES 4
+
+/**
+ * enum skip_source - Source of skip rule
+ */
+enum skip_source {
+	SKIP_SOURCE_BUILTIN,    /* From vendor built-in array */
+	SKIP_SOURCE_CONFIG,     /* From /etc/igt/platform_skip.conf */
+	SKIP_SOURCE_ENV,        /* From IGT_PLATFORM_SKIP_CONFIG */
+	SKIP_SOURCE_NONE,       /* Not skipped */
+};
+
+/**
+ * struct platform_skip_entry - Generic skip rule entry
+ *
+ * Generic structure for skip rules. Vendor-specific data is stored
+ * in platform_data field and interpreted by vendor callbacks.
+ */
+struct platform_skip_entry {
+	const char *test_name;          /* Test binary name or "*" for all */
+	const char *subtest_glob;       /* Subtest pattern (fnmatch) or "*" */
+	const char *reason;             /* Human-readable reason (required) */
+	void *platform_data;            /* Vendor-specific platform matching data */
+};
+
+/**
+ * struct platform_filter_ops - Vendor-specific operations
+ *
+ * Callback structure that vendors implement to provide platform-specific
+ * filtering logic. This allows the core filtering framework to remain
+ * vendor-agnostic.
+ */
+struct platform_filter_ops {
+	/** @name: Vendor name (e.g., "amd", "intel") */
+
+	const char *name;
+
+	/** @get_platform_name: Get current platform name */
+	const char *(*get_platform_name)(const void *platform_info);
+
+	/** @match_platform: Check if skip entry matches current platform */
+	bool (*match_platform)(const void *platform_info, const void *platform_data);
+
+	/** @parse_platform_config: Parse platform string from config file */
+	bool (*parse_platform_config)(const char *platform_str, void **platform_data_out);
+
+	/** @get_builtin_rules: Get vendor-specific built-in skip rules */
+	const struct platform_skip_entry *(*get_builtin_rules)(int *count_out);
+
+	/** @dump_platform_data: Dump platform_data for debugging (optional) */
+	void (*dump_platform_data)(const void *platform_data);
+};
+
+bool igt_platform_filter_is_initialized(void);
+
+void igt_platform_filter_init(const struct platform_filter_ops *ops,
+			       const void *platform_info);
+
+void igt_platform_require(const char *subtest_name);
+
+bool igt_platform_should_skip(const char *test_name,
+			       const char *subtest_name,
+			       enum skip_source *source,
+			       const char **reason);
+
+void igt_platform_filter_dump(void);
+
+int igt_platform_filter_dump_to_file(const char *filename);
+
+#endif /* IGT_PLATFORM_FILTER_H */
diff --git a/lib/meson.build b/lib/meson.build
index 3001b473e..1c04ee813 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -38,6 +38,7 @@ lib_sources = [
 	'igt_params.c',
 	'igt_perf.c',
 	'igt_pipe_crc.c',
+	'igt_platform_filter.c',
 	'igt_power.c',
 	'igt_primes.c',
 	'igt_pci.c',
-- 
2.43.0