julia_set/
opencl.rs

1use std::sync::{LazyLock, Mutex};
2
3use ocl::{
4    Buffer, Context, Device, OclPrm, Platform, ProQue, Queue, builders::BuildOpt, flags,
5    prm::Float2,
6};
7
8use crate::{Backend, Function, ImageBuffer, Params, Render, compiler::Compiler};
9
10const PROGRAM: &str = include_str!(concat!(env!("OUT_DIR"), "/program.cl"));
11
12/// Backend based on [OpenCL].
13///
14/// [OpenCL]: https://www.khronos.org/opencl/
15#[cfg_attr(docsrs, doc(cfg(feature = "opencl_backend")))]
16#[derive(Debug, Clone, Copy, Default)]
17pub struct OpenCl;
18
19impl Backend<&Function> for OpenCl {
20    type Error = ocl::Error;
21    type Program = OpenClProgram;
22
23    fn create_program(&self, function: &Function) -> Result<Self::Program, Self::Error> {
24        let compiled = Compiler::for_ocl().compile(function);
25        OpenClProgram::new(compiled)
26    }
27}
28
29/// Program produced by the [`OpenCl`] backend.
30#[cfg_attr(docsrs, doc(cfg(feature = "opencl_backend")))]
31#[derive(Debug)]
32pub struct OpenClProgram {
33    inner: ProQue,
34}
35
36impl OpenClProgram {
37    fn new(compiled: String) -> ocl::Result<Self> {
38        // For some reason, certain OpenCL implementations (e.g., POCL) do not work well
39        // when the list of devices for a platform is queried from multiple threads.
40        // Hence, we introduce a `Mutex` to serialize these calls.
41        static MUTEX: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
42
43        let mut program_builder = ocl::Program::builder();
44        let define = BuildOpt::IncludeDefine {
45            ident: "COMPUTE(z)".to_owned(),
46            val: compiled,
47        };
48        program_builder.bo(define).source(PROGRAM);
49
50        let (platform, device) = {
51            let _lock = MUTEX.lock().ok();
52            let platform = Platform::first()?;
53            (platform, Device::first(platform)?)
54        };
55
56        let context = Context::builder()
57            .platform(platform)
58            .devices(device)
59            .build()?;
60        let inner = ProQue::new(
61            context.clone(),
62            Queue::new(&context, device, None)?,
63            program_builder.build(&context)?,
64            None::<usize>,
65        );
66        Ok(Self { inner })
67    }
68}
69
70impl Render for OpenClProgram {
71    type Error = ocl::Error;
72
73    fn render(&self, params: &Params) -> Result<ImageBuffer, Self::Error> {
74        let pixels = params.image_size[0]
75            .checked_mul(params.image_size[1])
76            .expect("Overflow in image dimensions");
77        let buffer: Buffer<u8> = Buffer::builder()
78            .queue(self.inner.queue().clone())
79            .len(pixels)
80            .flags(flags::MEM_WRITE_ONLY | flags::MEM_HOST_READ_ONLY)
81            .build()?;
82
83        let cl_params = ClParams {
84            view_center: Float2::new(params.view_center[0], params.view_center[1]),
85            view_size: Float2::new(params.view_width(), params.view_height),
86            inf_distance_sq: params.inf_distance * params.inf_distance,
87            max_iterations: params.max_iterations,
88        };
89        let kernel = self
90            .inner
91            .kernel_builder("julia")
92            .arg_named("output", &buffer)
93            .arg_named("params", cl_params)
94            .build()?;
95
96        let command = kernel.cmd().global_work_size(params.image_size);
97        unsafe { command.enq()? };
98
99        let mut image = ImageBuffer::new(params.image_size[0], params.image_size[1]);
100        buffer.read(&mut *image).enq()?;
101        Ok(image)
102    }
103}
104
105#[derive(Debug, Clone, Copy, Default, PartialEq)]
106#[repr(C, packed)]
107struct ClParams {
108    view_center: Float2,
109    view_size: Float2,
110    inf_distance_sq: f32,
111    max_iterations: u8,
112}
113
114// Safety ensured by the same alignment here and in OCL code.
115unsafe impl OclPrm for ClParams {}