diff --git a/drivers/input/i2c-hidd/src/main.rs b/drivers/input/i2c-hidd/src/main.rs
new file mode 100644
index 00000000..88270e37
--- /dev/null
+++ b/drivers/input/i2c-hidd/src/main.rs
@@ -0,0 +1,114 @@
+use std::process;
+use std::thread;
+use std::time::Duration;
+
+use anyhow::{Context, Result};
+
+mod acpi;
+mod hid;
+mod input;
+mod quirks;
+
+use acpi::{
+ hid_descriptor_address, prepare_acpi_device, read_decoded_resources, recover_acpi_device,
+ scan_acpi_i2c_hid_devices,
+};
+use hid::{fetch_hid_descriptor, fetch_report_descriptor, stream_input_reports, I2cAdapterClient};
+use input::InputForwarder;
+use quirks::match_probe_failure_quirk;
+
+fn main() {
+ daemon::Daemon::new(daemon);
+}
+
+fn daemon(daemon: daemon::Daemon) -> ! {
+ common::setup_logging(
+ "input",
+ "i2c-hid",
+ "i2c-hidd",
+ common::output_level(),
+ common::file_level(),
+ );
+
+ if let Err(err) = run(daemon) {
+ log::error!("RB_I2C_HIDD_BLOCKER stage=startup error={err:#}");
+ process::exit(1);
+ }
+
+ process::exit(0);
+}
+
+fn run(daemon: daemon::Daemon) -> Result<()> {
+ log::info!("RB_I2C_HIDD_SCHEMA version=1");
+
+ let devices = scan_acpi_i2c_hid_devices().context("failed to scan ACPI I2C HID devices")?;
+ if devices.is_empty() {
+ log::warn!("RB_I2C_HIDD_BLOCKER stage=scan error=no PNP0C50/ACPI0C50 devices found");
+ }
+
+ let mut workers = Vec::new();
+ for device in devices {
+ log::info!("RB_I2C_HIDD_SNAPSHOT device={device}");
+ workers.push(thread::spawn(move || {
+ if let Err(err) = bind_device(&device) {
+ log::error!("RB_I2C_HIDD_BLOCKER device={} error={:#}", device, err);
+ }
+ }));
+ }
+
+ daemon.ready();
+
+ if workers.is_empty() {
+ loop {
+ thread::sleep(Duration::from_secs(5));
+ }
+ }
+
+ for worker in workers {
+ let _ = worker.join();
+ }
+ Ok(())
+}
+
+pub fn bind_device(device_path: &str) -> Result<()> {
+ prepare_acpi_device(device_path)
+ .with_context(|| format!("failed to prepare ACPI device {device_path}"))?;
+
+ let resources = read_decoded_resources(device_path)
+ .with_context(|| format!("failed to decode _CRS for {device_path}"))?;
+ log::info!(
+ "RB_I2C_HIDD_SNAPSHOT device={} adapter={} addr={:04x} irq={:?} gpio_int={} gpio_io={}",
+ device_path,
+ resources.i2c.adapter,
+ resources.i2c.address,
+ resources.irq,
+ resources.gpio_int.len(),
+ resources.gpio_io.len()
+ );
+
+ let hid_desc_addr = hid_descriptor_address(device_path)
+ .with_context(|| format!("failed to evaluate _DSM for {device_path}"))?;
+ let adapter = I2cAdapterClient::new(resources.i2c.clone());
+ let hid_desc = fetch_hid_descriptor(&adapter, resources.i2c.address, hid_desc_addr)
+ .with_context(|| format!("failed to fetch HID descriptor for {device_path}"))?;
+ let report_desc = fetch_report_descriptor(&adapter, resources.i2c.address, &hid_desc)
+ .with_context(|| format!("failed to fetch report descriptor for {device_path}"))?;
+ let mut forwarder = InputForwarder::new().context("failed to connect to inputd producer")?;
+
+ match stream_input_reports(
+ &adapter,
+ resources.i2c.address,
+ &hid_desc,
+ &report_desc,
+ &mut forwarder,
+ ) {
+ Ok(()) => Ok(()),
+ Err(err) => {
+ let quirk =
+ match_probe_failure_quirk().context("failed to evaluate DMI recovery quirks")?;
+ recover_acpi_device(device_path, &resources, quirk.as_ref())
+ .with_context(|| format!("failed ACPI recovery for {device_path}"))?;
+ Err(err).with_context(|| format!("streaming input reports failed for {device_path}"))
+ }
+ }
+}
diff --git a/drivers/input/intel-thc-hidd/src/main.rs b/drivers/input/intel-thc-hidd/src/main.rs
new file mode 100644
index 00000000..c5cda29e
--- /dev/null
+++ b/drivers/input/intel-thc-hidd/src/main.rs
@@ -0,0 +1,282 @@
+use std::collections::BTreeSet;
+use std::fs::{self, OpenOptions};
+use std::io::Read;
+use std::process;
+use std::thread;
+use std::time::Duration;
+
+use acpi_resource::ResourceDescriptor;
+use amlserde::{AmlSerde, AmlSerdeValue};
+use anyhow::{bail, Context, Result};
+use libredox::flag::{O_CLOEXEC, O_RDWR};
+use pcid_interface::PciFunctionHandle;
+
+mod quicki2c;
+mod thc;
+
+use quicki2c::QuickI2cTransport;
+use thc::{ThcController, SUPPORTED_PCI_IDS};
+
+fn main() {
+ pcid_interface::pci_daemon(daemon);
+}
+
+fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! {
+ common::setup_logging(
+ "input",
+ "intel-thc",
+ "intel-thc-hidd",
+ common::output_level(),
+ common::file_level(),
+ );
+
+ if let Err(err) = run(daemon, &mut pcid_handle) {
+ log::error!("RB_THC_HIDD_FATAL error={err:#}");
+ process::exit(1);
+ }
+
+ process::exit(0);
+}
+
+fn run(daemon: daemon::Daemon, pcid_handle: &mut PciFunctionHandle) -> Result<()> {
+ log::info!("RB_THC_HIDD_SCHEMA version=1");
+
+ let pci_config = pcid_handle.config();
+ let id = (
+ pci_config.func.full_device_id.vendor_id,
+ pci_config.func.full_device_id.device_id,
+ );
+ if !SUPPORTED_PCI_IDS.contains(&id) {
+ bail!("unsupported Intel THC PCI device {:04x}:{:04x}", id.0, id.1);
+ }
+
+ pcid_handle.enable_device();
+ let bar = unsafe { pcid_handle.map_bar(0) };
+ let controller = ThcController::new(bar.ptr.as_ptr(), bar.bar_size)
+ .context("failed to create THC controller")?;
+
+ let companion = resolve_acpi_companion(&pci_config.func.addr)
+ .context("failed to resolve ACPI companion for THC device")?;
+ let override_address = companion
+ .as_deref()
+ .map(companion_slave_address_override)
+ .transpose()
+ .context("failed to evaluate THC slave-address override")?
+ .flatten();
+ let hid_devices = scan_bound_i2c_hid_devices(companion.as_deref())
+ .context("failed to scan PNP0C50 devices for THC controller")?;
+
+ let effective_address = override_address.unwrap_or(0x0015);
+ let transport = QuickI2cTransport::new(controller, effective_address);
+ transport.prime_controller();
+ transport.emulate_transfer(&[]);
+ log::debug!("RB_THC_HIDD status={:#x}", transport.status());
+
+ match transport.register_with_i2cd(companion.as_deref(), override_address) {
+ Ok(()) => {}
+ Err(err) => {
+ log::warn!("RB_THC_HIDD registration error={err:#}");
+ }
+ }
+
+ log::info!(
+ "RB_THC_HIDD pci={} companion={:?} override={:?} hid_devices={}",
+ pci_config.func.name(),
+ companion,
+ override_address,
+ hid_devices.len()
+ );
+
+ daemon.ready();
+
+ loop {
+ thread::sleep(Duration::from_secs(5));
+ }
+}
+
+fn resolve_acpi_companion(addr: &pci_types::PciAddress) -> Result