diff --git a/docker/hwaccel.transcoding.yml b/docker/hwaccel.transcoding.yml
index 2f6ae3ebde..ef9c0a5bb1 100644
--- a/docker/hwaccel.transcoding.yml
+++ b/docker/hwaccel.transcoding.yml
@@ -1,9 +1,9 @@
 version: "3.8"
 
-# Configurations for hardware-accelerated transcoding 
+# Configurations for hardware-accelerated transcoding
 
 # If using Unraid or another platform that doesn't allow multiple Compose files,
-# you can inline the config for a backend by copying its contents 
+# you can inline the config for a backend by copying its contents
 # into the immich-microservices service in the docker-compose.yml file.
 
 # See https://immich.app/docs/features/hardware-transcoding for more info on using hardware transcoding.
@@ -38,6 +38,10 @@ services:
       - /dev/dri:/dev/dri
       - /dev/dma_heap:/dev/dma_heap
       - /dev/mpp_service:/dev/mpp_service
+      #- /dev/mali0:/dev/mali0 # only required to enable OpenCL-accelerated HDR -> SDR tonemapping
+    volumes:
+      #- /etc/OpenCL:/etc/OpenCL:ro # only required to enable OpenCL-accelerated HDR -> SDR tonemapping
+      #- /usr/lib/aarch64-linux-gnu/libmali.so.1:/usr/lib/aarch64-linux-gnu/libmali.so.1:ro # only required to enable OpenCL-accelerated HDR -> SDR tonemapping
 
   vaapi:
     devices:
diff --git a/docs/docs/features/hardware-transcoding.md b/docs/docs/features/hardware-transcoding.md
index db3d1ba7d6..420cd2a43b 100644
--- a/docs/docs/features/hardware-transcoding.md
+++ b/docs/docs/features/hardware-transcoding.md
@@ -42,6 +42,18 @@ You do not need to redo any transcoding jobs after enabling hardware acceleratio
   - If you have an 11th gen CPU or older, then you may need to follow [these][jellyfin-lp] instructions as Low-Power mode is required
   - Additionally, if the server specifically has an 11th gen CPU and is running kernel 5.15 (shipped with Ubuntu 22.04 LTS), then you will need to upgrade this kernel (from [Jellyfin docs][jellyfin-kernel-bug])
 
+#### RKMPP
+
+For RKMPP to work:
+
+- You must have a supported Rockchip ARM SoC.
+- Only RK3588 supports hardware tonemapping, other SoCs use slower software tonemapping while still using hardware encoding.
+- Tonemapping requires `/usr/lib/aarch64-linux-gnu/libmali.so.1` to be present on your host system. Install [`libmali-valhall-g610-g6p0-gbm`][libmali-rockchip] and modify the [`hwaccel.transcoding.yml`][hw-file] file:
+  - under `rkmpp` uncomment the 3 lines required for OpenCL tonemapping by removing the `#` symbol at the beginning of each line
+  - `- /dev/mali0:/dev/mali0`
+  - `- /etc/OpenCL:/etc/OpenCL:ro`
+  - `- /usr/lib/aarch64-linux-gnu/libmali.so.1:/usr/lib/aarch64-linux-gnu/libmali.so.1:ro`
+
 ## Setup
 
 #### Basic Setup
@@ -106,3 +118,4 @@ Once this is done, you can continue to step 3 of "Basic Setup".
 [nvcr]: https://github.com/NVIDIA/nvidia-container-runtime/
 [jellyfin-lp]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#configure-and-verify-lp-mode-on-linux
 [jellyfin-kernel-bug]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#known-issues-and-limitations
+[libmali-rockchip]: https://github.com/tsukumijima/libmali-rockchip/releases
diff --git a/server/src/domain/media/media.service.spec.ts b/server/src/domain/media/media.service.spec.ts
index 244978d099..8a6eae4cc1 100644
--- a/server/src/domain/media/media.service.spec.ts
+++ b/server/src/domain/media/media.service.spec.ts
@@ -23,6 +23,7 @@ import {
   personStub,
   probeStub,
 } from '@test';
+import { Stats } from 'node:fs';
 import { JobName } from '../job';
 import {
   IAssetRepository,
@@ -1853,6 +1854,41 @@ describe(MediaService.name, () => {
         },
       );
     });
+
+    it('should set OpenCL tonemapping options for rkmpp when OpenCL is available', async () => {
+      storageMock.readdir.mockResolvedValue(['renderD128']);
+      storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
+      mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
+      configMock.load.mockResolvedValue([
+        { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.RKMPP },
+        { key: SystemConfigKey.FFMPEG_CRF, value: 30 },
+        { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '0' },
+      ]);
+      assetMock.getByIds.mockResolvedValue([assetStub.video]);
+      await sut.handleVideoConversion({ id: assetStub.video.id });
+      expect(mediaMock.transcode).toHaveBeenCalledWith(
+        '/original/path.ext',
+        'upload/encoded-video/user-id/as/se/asset-id.mp4',
+        {
+          inputOptions: ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga'],
+          outputOptions: [
+            `-c:v h264_rkmpp`,
+            '-c:a copy',
+            '-movflags faststart',
+            '-fps_mode passthrough',
+            '-map 0:0',
+            '-map 0:1',
+            '-g 256',
+            '-v verbose',
+            '-vf scale_rkrga=-2:720:format=p010:afbc=1,hwmap=derive_device=opencl:mode=read,tonemap_opencl=format=nv12:r=pc:p=bt709:t=bt709:m=bt709:tonemap=hable:desat=0,hwmap=derive_device=rkmpp:mode=write:reverse=1,format=drm_prime',
+            '-level 51',
+            '-rc_mode CQP',
+            '-qp_init 30',
+          ],
+          twoPass: false,
+        },
+      );
+    });
   });
 
   it('should tonemap when policy is required and video is hdr', async () => {
diff --git a/server/src/domain/media/media.service.ts b/server/src/domain/media/media.service.ts
index 6a5c8ff9d3..5c8e777ad5 100644
--- a/server/src/domain/media/media.service.ts
+++ b/server/src/domain/media/media.service.ts
@@ -47,6 +47,7 @@ export class MediaService {
   private logger = new ImmichLogger(MediaService.name);
   private configCore: SystemConfigCore;
   private storageCore: StorageCore;
+  private hasOpenCL?: boolean = undefined;
 
   constructor(
     @Inject(IAssetRepository) private assetRepository: IAssetRepository,
@@ -456,8 +457,19 @@ export class MediaService {
         break;
       }
       case TranscodeHWAccel.RKMPP: {
+        if (this.hasOpenCL === undefined) {
+          try {
+            const maliIcdStat = await this.storageRepository.stat('/etc/OpenCL/vendors/mali.icd');
+            const maliDeviceStat = await this.storageRepository.stat('/dev/mali0');
+            this.hasOpenCL = maliIcdStat.isFile() && maliDeviceStat.isCharacterDevice();
+          } catch {
+            this.logger.warn('OpenCL not available for transcoding, using CPU instead.');
+            this.hasOpenCL = false;
+          }
+        }
+
         devices = await this.storageRepository.readdir('/dev/dri');
-        handler = new RKMPPConfig(config, devices);
+        handler = new RKMPPConfig(config, devices, this.hasOpenCL);
         break;
       }
       default: {
diff --git a/server/src/domain/media/media.util.ts b/server/src/domain/media/media.util.ts
index d5f08ab0de..3acabb4356 100644
--- a/server/src/domain/media/media.util.ts
+++ b/server/src/domain/media/media.util.ts
@@ -608,6 +608,17 @@ export class VAAPIConfig extends BaseHWConfig {
 }
 
 export class RKMPPConfig extends BaseHWConfig {
+  private hasOpenCL: boolean;
+
+  constructor(
+    protected config: SystemConfigFFmpegDto,
+    devices: string[] = [],
+    hasOpenCL: boolean = false,
+  ) {
+    super(config, devices);
+    this.hasOpenCL = hasOpenCL;
+  }
+
   eligibleForTwoPass(): boolean {
     return false;
   }
@@ -616,19 +627,25 @@ export class RKMPPConfig extends BaseHWConfig {
     if (this.devices.length === 0) {
       throw new Error('No RKMPP device found');
     }
-    if (this.shouldToneMap(videoStream)) {
-      // disable hardware decoding
-      return [];
-    }
-    return ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga'];
+    return this.shouldToneMap(videoStream) && !this.hasOpenCL
+      ? [] // disable hardware decoding & filters
+      : ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga'];
   }
 
   getFilterOptions(videoStream: VideoStreamInfo) {
     if (this.shouldToneMap(videoStream)) {
-      // use software filter options
-      return super.getFilterOptions(videoStream);
-    }
-    if (this.shouldScale(videoStream)) {
+      if (!this.hasOpenCL) {
+        return super.getFilterOptions(videoStream);
+      }
+      const colors = this.getColors();
+      return [
+        `scale_rkrga=${this.getScaling(videoStream)}:format=p010:afbc=1`,
+        'hwmap=derive_device=opencl:mode=read',
+        `tonemap_opencl=format=nv12:r=pc:p=${colors.primaries}:t=${colors.transfer}:m=${colors.matrix}:tonemap=${this.config.tonemap}:desat=0`,
+        'hwmap=derive_device=rkmpp:mode=write:reverse=1',
+        'format=drm_prime',
+      ];
+    } else if (this.shouldScale(videoStream)) {
       return [`scale_rkrga=${this.getScaling(videoStream)}:format=nv12:afbc=1`];
     }
     return [];