View Javadoc
1   package net.bmahe.genetics4j.gpu.spec.fitness;
2   
3   import java.util.Map;
4   import java.util.Objects;
5   
6   import org.apache.commons.lang3.Validate;
7   import org.apache.logging.log4j.LogManager;
8   import org.apache.logging.log4j.Logger;
9   import org.jocl.CL;
10  import org.jocl.Pointer;
11  import org.jocl.Sizeof;
12  
13  import net.bmahe.genetics4j.gpu.opencl.OpenCLExecutionContext;
14  import net.bmahe.genetics4j.gpu.opencl.model.Device;
15  import net.bmahe.genetics4j.gpu.spec.fitness.cldata.CLData;
16  
17  /**
18   * Utility class for extracting computation results from OpenCL device memory after GPU kernel execution.
19   * 
20   * <p>ResultExtractor provides type-safe methods for retrieving different data types from OpenCL memory buffers that
21   * contain the results of GPU-accelerated fitness evaluation. This class handles the device-to-host data transfer and
22   * type conversion necessary to make GPU computation results available to the evolutionary algorithm.
23   * 
24   * <p>Key functionality includes:
25   * <ul>
26   * <li><strong>Type-safe extraction</strong>: Methods for extracting float, int, long arrays with type validation</li>
27   * <li><strong>Image data support</strong>: Specialized extraction for OpenCL image objects</li>
28   * <li><strong>Device management</strong>: Tracks result data across multiple devices</li>
29   * <li><strong>Argument indexing</strong>: Maps kernel arguments to their corresponding result data</li>
30   * </ul>
31   * 
32   * <p>Common usage patterns:
33   * 
34   * <pre>{@code
35   * // Extract fitness values as float array
36   * float[] fitnessValues = resultExtractor.extractFloatArray(context, 0);
37   * 
38   * // Extract integer results (e.g., classification results)
39   * int[] classifications = resultExtractor.extractIntArray(context, 1);
40   * 
41   * // Extract long results (e.g., counters or large indices)
42   * long[] counters = resultExtractor.extractLongArray(context, 2);
43   * 
44   * // Extract image data for visualization
45   * byte[] imageData = resultExtractor.extractImageAsByteArray(context, 3, width, height, channels, channelSize);
46   * 
47   * // Use extracted results in fitness evaluation
48   * List<Double> fitness = IntStream.range(0, fitnessValues.length)
49   * 		.mapToDouble(i -> (double) fitnessValues[i])
50   * 		.boxed()
51   * 		.collect(Collectors.toList());
52   * }</pre>
53   * 
54   * <p>Data extraction workflow:
55   * <ol>
56   * <li><strong>Kernel execution</strong>: GPU kernels compute results and store them in device memory</li>
57   * <li><strong>Result mapping</strong>: Results are mapped by device and kernel argument index</li>
58   * <li><strong>Type validation</strong>: Data types are validated before extraction</li>
59   * <li><strong>Data transfer</strong>: Results are transferred from device to host memory</li>
60   * <li><strong>Type conversion</strong>: Data is converted to appropriate Java types</li>
61   * </ol>
62   * 
63   * <p>Error handling and validation:
64   * <ul>
65   * <li><strong>Device validation</strong>: Ensures requested device has result data</li>
66   * <li><strong>Argument validation</strong>: Validates argument indices exist in result mapping</li>
67   * <li><strong>Type checking</strong>: Ensures extracted data matches expected OpenCL types</li>
68   * <li><strong>Transfer validation</strong>: Validates successful device-to-host data transfer</li>
69   * </ul>
70   * 
71   * <p>Performance considerations:
72   * <ul>
73   * <li><strong>Synchronous transfers</strong>: Uses blocking transfers to ensure data availability</li>
74   * <li><strong>Memory efficiency</strong>: Allocates host memory based on actual data sizes</li>
75   * <li><strong>Transfer optimization</strong>: Minimizes number of device-to-host transfers</li>
76   * <li><strong>Type safety</strong>: Validates types at runtime to prevent data corruption</li>
77   * </ul>
78   * 
79   * @see CLData
80   * @see net.bmahe.genetics4j.gpu.spec.fitness.OpenCLFitness
81   * @see OpenCLExecutionContext
82   */
83  public class ResultExtractor {
84  	public static final Logger logger = LogManager.getLogger(ResultExtractor.class);
85  
86  	private final Map<Device, Map<Integer, CLData>> resultData;
87  
88  	/**
89  	 * Extracts CLData for the specified device and kernel argument index.
90  	 * 
91  	 * @param device        the OpenCL device to extract data from
92  	 * @param argumentIndex the kernel argument index for the data
93  	 * @return the CLData object containing the result data
94  	 * @throws IllegalArgumentException if device is null, argumentIndex is negative, device not found, or argument index
95  	 *                                  not found
96  	 */
97  	protected CLData extractClData(final Device device, final int argumentIndex) {
98  		Objects.requireNonNull(device);
99  		Validate.isTrue(argumentIndex >= 0);
100 
101 		if (resultData.containsKey(device) == false) {
102 			throw new IllegalArgumentException("Could not find entry for device [" + device.name() + "]");
103 		}
104 
105 		final var deviceResults = resultData.get(device);
106 
107 		if (deviceResults.containsKey(argumentIndex) == false) {
108 			throw new IllegalArgumentException("No data defined for argument " + argumentIndex);
109 		}
110 
111 		return deviceResults.get(argumentIndex);
112 	}
113 
114 	/**
115 	 * Constructs a ResultExtractor with the specified result data mapping.
116 	 * 
117 	 * @param _resultData mapping from devices to their kernel argument results
118 	 */
119 	public ResultExtractor(final Map<Device, Map<Integer, CLData>> _resultData) {
120 
121 		this.resultData = _resultData;
122 	}
123 
124 	/**
125 	 * Extracts image data from OpenCL device memory as a byte array.
126 	 * 
127 	 * <p>This method reads an OpenCL image object from device memory and converts it to a byte array suitable for host
128 	 * processing. The image dimensions and channel information must be provided to properly interpret the image data.
129 	 * 
130 	 * @param openCLExecutionContext the OpenCL execution context
131 	 * @param argumentIndex          the kernel argument index containing the image data
132 	 * @param width                  the image width in pixels
133 	 * @param height                 the image height in pixels
134 	 * @param numChannels            the number of color channels (e.g., 3 for RGB, 4 for RGBA)
135 	 * @param channelSize            the size of each channel in bytes
136 	 * @return byte array containing the image data
137 	 * @throws IllegalArgumentException if any parameter is invalid
138 	 */
139 	public byte[] extractImageAsByteArray(final OpenCLExecutionContext openCLExecutionContext, final int argumentIndex,
140 			final int width, final int height, final int numChannels, final int channelSize) {
141 		Validate.isTrue(argumentIndex >= 0);
142 		Validate.isTrue(width > 0);
143 		Validate.isTrue(height > 0);
144 		Validate.isTrue(numChannels > 0);
145 		Validate.isTrue(channelSize > 0);
146 
147 		final var device = openCLExecutionContext.device();
148 		final var clData = extractClData(device, argumentIndex);
149 
150 		final var clCommandQueue = openCLExecutionContext.clCommandQueue();
151 
152 		final byte[] data = new byte[width * height * numChannels * channelSize];
153 		CL.clEnqueueReadImage(
154 				clCommandQueue,
155 					clData.clMem(),
156 					CL.CL_TRUE,
157 					new long[] { 0, 0, 0 },
158 					new long[] { width, height, 1 },
159 					0,
160 					0,
161 					Pointer.to(data),
162 					0,
163 					null,
164 					null);
165 
166 		return data;
167 	}
168 
169 	/**
170 	 * Extracts floating-point data from OpenCL device memory as a float array.
171 	 * 
172 	 * <p>This method reads floating-point data from device memory and transfers it to host memory. The data type is
173 	 * validated to ensure it contains floating-point values before extraction.
174 	 * 
175 	 * @param openCLExecutionContext the OpenCL execution context
176 	 * @param argumentIndex          the kernel argument index containing the float data
177 	 * @return float array containing the extracted data
178 	 * @throws IllegalArgumentException if the data is not of type float
179 	 */
180 	public float[] extractFloatArray(final OpenCLExecutionContext openCLExecutionContext, final int argumentIndex) {
181 		final var device = openCLExecutionContext.device();
182 		final var clData = extractClData(device, argumentIndex);
183 
184 		if (clData.clType() != Sizeof.cl_float) {
185 			throw new IllegalArgumentException("Data is not of type of float[]");
186 		}
187 
188 		final var clCommandQueue = openCLExecutionContext.clCommandQueue();
189 
190 		final float[] data = new float[clData.size()];
191 		CL.clEnqueueReadBuffer(
192 				clCommandQueue,
193 					clData.clMem(),
194 					CL.CL_TRUE,
195 					0,
196 					clData.size() * Sizeof.cl_float,
197 					Pointer.to(data),
198 					0,
199 					null,
200 					null);
201 
202 		return data;
203 	}
204 
205 	/**
206 	 * Extracts integer data from OpenCL device memory as an int array.
207 	 * 
208 	 * <p>This method reads integer data from device memory and transfers it to host memory. The data type is validated
209 	 * to ensure it contains integer values before extraction.
210 	 * 
211 	 * @param openCLExecutionContext the OpenCL execution context
212 	 * @param argumentIndex          the kernel argument index containing the integer data
213 	 * @return int array containing the extracted data
214 	 * @throws IllegalArgumentException if the data is not of type int
215 	 */
216 	public int[] extractIntArray(final OpenCLExecutionContext openCLExecutionContext, final int argumentIndex) {
217 		final var device = openCLExecutionContext.device();
218 		final var clData = extractClData(device, argumentIndex);
219 
220 		if (clData.clType() != Sizeof.cl_int) {
221 			throw new IllegalArgumentException("Data is not of type of int[]");
222 		}
223 
224 		final var clCommandQueue = openCLExecutionContext.clCommandQueue();
225 
226 		final int[] data = new int[clData.size()];
227 		CL.clEnqueueReadBuffer(
228 				clCommandQueue,
229 					clData.clMem(),
230 					CL.CL_TRUE,
231 					0,
232 					clData.size() * Sizeof.cl_int,
233 					Pointer.to(data),
234 					0,
235 					null,
236 					null);
237 
238 		return data;
239 	}
240 
241 	/**
242 	 * Extracts long integer data from OpenCL device memory as a long array.
243 	 * 
244 	 * <p>This method reads long integer data from device memory and transfers it to host memory. The data type is
245 	 * validated to ensure it contains long integer values before extraction.
246 	 * 
247 	 * @param openCLExecutionContext the OpenCL execution context
248 	 * @param argumentIndex          the kernel argument index containing the long integer data
249 	 * @return long array containing the extracted data
250 	 * @throws IllegalArgumentException if the data is not of type long
251 	 */
252 	public long[] extractLongArray(final OpenCLExecutionContext openCLExecutionContext, final int argumentIndex) {
253 		final var device = openCLExecutionContext.device();
254 		final var clData = extractClData(device, argumentIndex);
255 
256 		if (clData.clType() != Sizeof.cl_long) {
257 			throw new IllegalArgumentException("Data is not of type of long[]");
258 		}
259 
260 		final var clCommandQueue = openCLExecutionContext.clCommandQueue();
261 
262 		final long[] data = new long[clData.size()];
263 		CL.clEnqueueReadBuffer(
264 				clCommandQueue,
265 					clData.clMem(),
266 					CL.CL_TRUE,
267 					0,
268 					clData.size() * Sizeof.cl_long,
269 					Pointer.to(data),
270 					0,
271 					null,
272 					null);
273 		return data;
274 	}
275 }