On 10/1/2026 1:50 AM, Krzysztof Kozlowski wrote:
On Wed, Sep 30, 2026 at 07:52:11AM -0700, Georgi Djakov wrote:Add a platform driver for the Qualcomm Page Compression Engine (QPaCE), a hardware block that accelerates compression and decompression of memory pages.Provide the urgent command path for synchronous single-page compression and decompression. This exposes the low-latency operations needed by compressed-memory users such as zram, especially for page decompression on the read path. Signed-off-by: Georgi Djakov <[email protected]> --- drivers/soc/qcom/Kconfig | 14 + drivers/soc/qcom/Makefile | 1 + drivers/soc/qcom/qpace.c | 764 ++++++++++++++++++++++++++++++ drivers/soc/qcom/qpace_internal.h | 84 ++++ include/linux/soc/qcom/qpace.h | 154 ++++++ 5 files changed, 1017 insertions(+) create mode 100644 drivers/soc/qcom/qpace.c create mode 100644 drivers/soc/qcom/qpace_internal.h create mode 100644 include/linux/soc/qcom/qpace.h diff --git a/drivers/soc/qcom/Kconfig b/drivers/soc/qcom/Kconfig index 535c8619197b..6bcb86dcd726 100644 --- a/drivers/soc/qcom/KconfigSorry, but no. Soc is not a dumping ground. This has clear function of compression offload, so it should have some dedicated maintainers like other offload engines.
The reason for putting this in soc/qcom is because this is a qcom HW block driver. As per your comments below we will check and see if we can make use of existing crypto framework and respond back on this.
+++ b/drivers/soc/qcom/Kconfig @@ -288,6 +288,20 @@ config QCOM_PBS This module provides the APIs to the client drivers that wants to send the PBS trigger event to the PBS RAM.+config QCOM_PAGE_COMPRESSION_ENGINE+ tristate "Qualcomm Page Compression Engine (QPaCE)" + depends on ARM64Why this can't be built on other archs? This is really odd and I do not see any asm headers included.
ACK. We will remove this so that it can be built on other architectures.
+ depends on ARCH_QCOM || COMPILE_TEST + depends on OF + depends on INTERCONNECT + help + Enable support for the Qualcomm Page Compression Engine (QPaCE), + a hardware accelerator that provides high-throughput page compression, + decompression, and DMA copy operations. + + The engine is used as a hardware backend for compressed-memory + subsystems such as zram. If unsure, say N. +...+ ret = FIELD_GET(URG_CMD_0_ED_STAT_SIZE, stat_reg); +out: + return ret; +} +EXPORT_SYMBOL_GPL(qpace_urgent_compress); + +int qpace_urgent_decompress(dma_addr_t input_addr, + dma_addr_t output_addr, + size_t input_size, + struct qpace_algorithm *algo)You need kerneldoc for every export.
ACK
+{ + int urg_reg_num; + int stat_reg; + u32 stat_reg_val; + int ret; + + ret = qpace_get(); + if (ret) + goto out; + + urg_reg_num = get_cpu() % NUM_TRS_ERS_URG_CMD_REGS; + qpace_write_urg_cmd_ctx(qpace_priv, QPACE_URG_CMD_0_CFG_CNTXT_SIZE_n_OFFSET, + urg_reg_num, algo->urg_decomp_cntxt, + FIELD_PREP(URG_CMD_0_CFG_CNTXT_SIZE_SIZE, input_size)); + stat_reg = qpace_urgent_command_trigger(input_addr, output_addr, urg_reg_num, + algo->urg_decomp_cntxt); + put_cpu(); + + qpace_put(); + + if (stat_reg < 0) { + ret = stat_reg; + goto out; + } + + stat_reg_val = FIELD_GET(URG_CMD_0_ED_STAT_COMP_CODE, stat_reg); + if (stat_reg_val != OP_OK) { + pr_err("%s: register %d failed with %u\n", + __func__, urg_reg_num, stat_reg_val); + ret = -EINVAL; + goto out; + } + + ret = FIELD_GET(URG_CMD_0_ED_STAT_SIZE, stat_reg); +out: + return ret; +} +EXPORT_SYMBOL_GPL(qpace_urgent_decompress); +So singleton? For what reason exactly? Random drivers will be getting the reference to compress something? If so, aren't you duplicating existing infrastructure/API for in-kernel hardware offloaded compression (e.g. drivers/crypto/)?
We will check and see if we can use existing crypto framework and respond back on this.
You miss proper comments (see checkpatch --strict) explaining lock usage.
ACK
+static DEFINE_MUTEX(qpace_ref_lock); + +static void _get_qpace(void) +{ + lockdep_assert_held(&qpace_ref_lock); + if (!qpace_priv->active_rings) { + reinit_completion(&qpace_priv->no_active_refs); + pm_stay_awake(qpace_priv->dev); + cpu_latency_qos_update_request(&qpace_priv->qos_req, 300); + program_urg_command_contexts_v2(); + program_decomp_core_cfg(); + } + qpace_priv->active_rings++; +} + +static void _put_qpace(void) +{ + lockdep_assert_held(&qpace_ref_lock); + if (!--qpace_priv->active_rings) { + cpu_latency_qos_update_request(&qpace_priv->qos_req, PM_QOS_DEFAULT_VALUE); + pm_relax(qpace_priv->dev); + complete(&qpace_priv->no_active_refs); + } +} + +int qpace_get(void) +{ + int ret = 0; + + mutex_lock(&qpace_ref_lock); + if (qpace_priv->suspended || READ_ONCE(qpace_priv->broken)) + ret = -EBUSY; + else + _get_qpace(); + mutex_unlock(&qpace_ref_lock); + return ret; +} +EXPORT_SYMBOL_GPL(qpace_get); + +void qpace_put(void) +{ + mutex_lock(&qpace_ref_lock); + _put_qpace(); + mutex_unlock(&qpace_ref_lock); +} +EXPORT_SYMBOL_GPL(qpace_put); + +static irqreturn_t urgent_interrupt_handler(int irq, void *unused) +{ + pr_debug("Urgent interrupt handled\n"); + return IRQ_HANDLED; +} + +static int qpace_hw_init(void) +{ + u32 reg_val; + + /* Select CPU SCID for our system cache slice. */ + reg_val = qpace_read_gen(qpace_priv, QPACE_CORE_QNS4_CFG_OFFSET); + reg_val = u32_replace_bits(reg_val, 0x1, CORE_QNS4_CFG_CACHEINDEX); + qpace_write_gen(qpace_priv, QPACE_CORE_QNS4_CFG_OFFSET, reg_val); + + /* QMB2 register configurations. */ + reg_val = qpace_read_gen(qpace_priv, QPACE_CORE_GEN_CFG_OFFSET); + reg_val = u32_replace_bits(reg_val, 0x48, CORE_GEN_CFG_QMB2_MAX_RD_OUTST_LIMIT); + reg_val = u32_replace_bits(reg_val, 0x48, CORE_GEN_CFG_QMB2_MAX_WR_OUTST_LIMIT); + qpace_write_gen(qpace_priv, QPACE_CORE_GEN_CFG_OFFSET, reg_val); + + /* DECOMP_CORE_CFG init steps. */ + program_decomp_core_cfg(); + + /* Below settings help save power since all decomp cores are set to sync. */ + reg_val = qpace_read_gen_core(qpace_priv, QPACE_CORE_OPER_CFG_OFFSET); + reg_val |= CORE_OPER_CFG_COMP_MEM_PWR_DWN_1; + qpace_write_gen_core(qpace_priv, QPACE_CORE_OPER_CFG_OFFSET, reg_val); + + reg_val = qpace_read_comp_core(qpace_priv, QPACE_COMP_CORE_CFG_OFFSET); + reg_val = u32_replace_bits(reg_val, 0x8, COMP_CORE_CFG_DMA_RD_MAX_OT); + reg_val = u32_replace_bits(reg_val, 0x8, COMP_CORE_CFG_DMA_WR_MAX_OT); + qpace_write_comp_core(qpace_priv, QPACE_COMP_CORE_CFG_OFFSET, reg_val); + + /* Set all COMP engines to bulk mode. */ + reg_val = qpace_read_comp_core(qpace_priv, QPACE_COMP_CORE_BULK_MODE_OFFSET); + reg_val |= COMP_CORE_BULK_MODE_ALL_CORES; + qpace_write_comp_core(qpace_priv, QPACE_COMP_CORE_BULK_MODE_OFFSET, reg_val); + + /* URG CMD register configurations. */ + program_urg_command_contexts_v2(); + + return 0; +} + +enum qpace_interrupts { + QPACE_IRQ_URGENT +}; + +static int qpace_register_interrupts(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + int irq, ret; + + irq = platform_get_irq(pdev, QPACE_IRQ_URGENT); + if (irq < 0) + return irq; + + ret = devm_request_irq(dev, irq, urgent_interrupt_handler, + 0, "qpace-urgent-irq", NULL); + if (ret) + dev_err(dev, "failed to request urgent interrupt\n"); + + return ret; +} + +static inline bool _qpace_power_on(void) +{ + u32 ready_status; + + qpace_write_gen_core(qpace_priv, QPACE_CORE_OPER_CORE_RUN_STOP_OFFSET, QPACE_RUN); + + if (readl_poll_timeout(qpace_priv->gen_core_regs + + QPACE_CORE_OPER_CORE_READY_OFFSET, + ready_status, ready_status, + 1000, 5 * QPACE_STATE_CHANGE_TIMEOUT_US)) { + pr_err("Timeout in waiting for QPaCE to turn on\n"); + return false; + } + + return true; +} + +static int qpace_power_on(struct device *dev) +{ + int ret, ret2; + + qpace_priv->interconnect = devm_of_icc_get(dev, "qpace-mem"); + if (IS_ERR_OR_NULL(qpace_priv->interconnect)) { + ret = PTR_ERR_OR_ZERO(qpace_priv->interconnect); + pr_err("%s: devm_of_icc_get() failed with %d\n", __func__, ret);use dev_err, not pr_err
ACK.
+ return qpace_priv->interconnect ? ret : -EINVAL; + } + + ret = device_init_wakeup(dev, true); + if (ret) { + pr_err("%s: device_init_wakeup() failed with %d\n", __func__, ret); + return ret; + } + + cpu_latency_qos_add_request(&qpace_priv->qos_req, PM_QOS_DEFAULT_VALUE); + + icc_set_tag(qpace_priv->interconnect, QCOM_ICC_TAG_ACTIVE_ONLY); + + ret = icc_set_bw(qpace_priv->interconnect, 0, 1); + if (ret) { + pr_err("Failed to turn on QPaCE VCD: %d\n", ret); + goto rm_qos; + } + + if (!_qpace_power_on()) { + pr_err("Failed to start QPaCE\n"); + ret = -EINVAL; + goto rm_bw; + } + + return 0; + +rm_bw: + ret2 = icc_set_bw(qpace_priv->interconnect, 0, 0); + if (ret2) + pr_err("Failed to remove QPaCE VCD vote: %d\n", ret2); +rm_qos: + cpu_latency_qos_remove_request(&qpace_priv->qos_req); + device_init_wakeup(dev, false); + + return ret; +} + +static inline bool _qpace_power_off(void) +{ + u32 ready_status; + + qpace_write_gen_core(qpace_priv, QPACE_CORE_OPER_CORE_RUN_STOP_OFFSET, QPACE_STOP); + + if (readl_poll_timeout(qpace_priv->gen_core_regs + + QPACE_CORE_OPER_CORE_READY_OFFSET, + ready_status, !ready_status, + 1000, 5 * QPACE_STATE_CHANGE_TIMEOUT_US)) { + pr_err("Timeout in waiting for QPaCE to turn off\n"); + return false; + } + + return true; +} + +static void qpace_power_off(struct device *dev) +{ + int ret; + + /* If this fails we can still remove our vote for the VCD to turn QPaCE off */ + if (!_qpace_power_off()) + pr_err("Failed to stop QPaCE\n"); + + ret = icc_set_bw(qpace_priv->interconnect, 0, 0); + if (ret) + pr_err("Failed to turn off QPaCE VCD: %d\n", ret); + + cpu_latency_qos_remove_request(&qpace_priv->qos_req); + + device_init_wakeup(dev, false); +} + +static inline int qpace_register_ioremap(struct platform_device *pdev) +{ + qpace_priv->gen_regs = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(qpace_priv->gen_regs)) + return PTR_ERR(qpace_priv->gen_regs); + + qpace_priv->gen_core_regs = qpace_priv->gen_regs + QPACE_GEN_CORE_REGS_OFFSET; + qpace_priv->comp_core_regs = qpace_priv->gen_regs + QPACE_COMP_CORE_REGS_OFFSET; + qpace_priv->decomp_core_regs = qpace_priv->gen_regs + QPACE_DECOMP_CORE_REGS_OFFSET; + qpace_priv->urg_regs = qpace_priv->gen_regs + QPACE_URG_REGS_OFFSET; + + return 0; +} + +bool qpace_is_dev_available(void) +{ + return static_branch_likely(&qpace_drv_probed) && + !READ_ONCE(qpace_priv->broken); +} +EXPORT_SYMBOL_GPL(qpace_is_dev_available); + +struct device *qpace_get_dma_dev(void) +{ + return qpace_priv->dev; +} +EXPORT_SYMBOL_GPL(qpace_get_dma_dev); + +static int qpace_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct qpace_priv *priv; + int ret; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->dev = dev; + /* Starts already complete since active_rings == 0 at init. */ + init_completion(&priv->no_active_refs); + complete(&priv->no_active_refs); + INIT_WORK(&priv->disable_work, qpace_disable_work_fn); + qpace_priv = priv; + platform_set_drvdata(pdev, priv); + + ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64)); + if (ret) + return dev_err_probe(dev, ret, "failed to set DMA mask\n"); + + ret = qpace_register_ioremap(pdev); + if (ret) + return dev_err_probe(dev, ret, "failed to map QPaCE registers\n"); + + ret = qpace_power_on(dev); + if (ret) + return dev_err_probe(dev, ret, "failed to power on QPaCE\n"); + + /* Get QPaCE HW version. */ + qpace_priv->hw_version = qpace_read_gen(qpace_priv, QPACE_CORE_HW_VERSION_OFFSET); + if (qpace_priv->hw_version != QPACE_HW_VERSION_V2) { + dev_err(dev, "Unsupported QPaCE HW version returned: 0x%x\n", + qpace_priv->hw_version); + ret = -EINVAL; + goto power_off; + } + + ret = qpace_hw_init(); + if (ret) {How is this possible?
ACK. This can be removed.
+ dev_err(dev, "init failed: (%d)\n", ret); + goto power_off; + } + + ret = qpace_register_interrupts(pdev); + if (ret) { + dev_err(dev, "failed to register interrupts\n");Do not print same error multiple times.
ACK.
+ goto power_off; + } + + static_branch_enable(&qpace_drv_probed); + + return ret; + +power_off: + qpace_power_off(dev); + + return ret; +}Best regards, Krzysztof
