Данная справка относится к модулю OpenCLABC
, входящему в состав стандартных модулей языка PascalABC.Net
.
Модуль OpenCLABC
это высокоуровневая оболочка модуля OpenCL
.
Это значит, что с OpenCLABC
можно писать гораздо меньше кода в больших и сложных программах,
однако такой же уровень микроконтроля как с модулем OpenCL
недоступен.
Например, напрямую управлять cl_event
'ами в OpenCLABC
невозможно.
Вместо этого надо использовать операции с очередями (как сложение и умножение очередей)
Справка модуля OpenCL
отсутствует. Вместо неё смотрите:
OpenCL
и OpenGL
OpenCL
, на которой основан модуль OpenCL
Если в найдена ошибка, или чего-либо не хватает (в справке или модуле) - пишите в issue.
CPU — Центральное Процессорное Устройство (процессор)
GPU — Графическое Процессорное Устройство (видеокарта)
Команда — запрос на выполнение чего-либо. К примеру:
Называть процедуры и функции командами ошибочно!
Подпрограмма — процедура или функция
Метод — особая подпрограмма, вызываемая через экземпляр
Context.SyncInvoke
выглядит в коде как cont.SyncInvoke(...)
, где cont
— переменная типа Context
Остальные непонятные термины ищите в справке PascalABC.NET
или в интернете
Для отправки команд в GPU необходим контекст (объект типа Context
):
Он содержит информацию о том, какое устройство будет использоваться при выполнении программ и хранении содержимого буферов.
Создать контекст можно конструктором (new Context(...)
).
Контекст можно и не создавать, используя всюду свойство Context.Default
.
Изначально этому свойству присваивается контекст, использующий один любой GPU, если таковой есть, или одно любое другое устройство, поддерживающее OpenCL, если GPU отсутствует.
Если устройств поддерживающих OpenCL
нет - Context.Default
будет nil
.
Однако такая ситуация теоретически невозможна, потому что OpenCL поддерживается
всеми современными устройствами, занимающимися выводом картинки на экран.
Если Context.Default=nil
- переустановите графические драйверы.
Context.Default
можно перезаписывать. Это может понадобиться, если во всей программе использовать общий контекст, но не стандартный.
Операции, у которых невозможно указать контекст - всегда используют Context.Default
.
Контекст используется только для инициализации буферов и выполнения команд на GPU.
Передавать команды для GPU по одной не эффективно. Гораздо эффективнее передавать несколько команд сразу.
Для этого существуют очереди (типы, наследующие от CommandQueue<T>
и CommandQueueBase
).
Они хранят произвольное количество команд для GPU.
А при необходимости также и части кода, выполняемые на CPU.
У каждого типа-очереди есть свой тип возвращаемого значение.
К примеру, так объявляется переменная в которую можно будет сохранить очередь, возвращающую integer
:
var Q1: CommandQueue<integer>;
Очереди, созданные из буфера или kernel-а возващают свой Buffer
/Kernel
соответственно, из которого были созданы;
Очереди, созданные с HFQ
- значение, которое вернёт переданная функция;
Очереди, созданные с HPQ
- значение типа object
(и всегда nil
).
К примеру:
uses OpenCLABC;
/// Вывод типа и значения объекта
procedure OtpObject(o: object) :=
Writeln( $'{o?.GetType}[{_ObjectToString(o)}]' );
// "o?.GetType" это короткая форма "o=nil ? nil : o.GetType", то есть берём или тип объекта, или nil если сам объект nil
// _ObjectToString это функция, которую использует Writeln для форматирования значений
begin
var b0 := new Buffer(1);
// Тип - буфер, потому что очередь создали из буфера
OtpObject( Context.Default.SyncInvoke( b0.NewQueue as CommandQueue<Buffer> ) );
// Тип - Int32 (то есть integer), потому что это тип по-умолчанию для выражения (5)
OtpObject( Context.Default.SyncInvoke( HFQ( ()->5 ) ) );
// Тип - string, по той же причине
OtpObject( Context.Default.SyncInvoke( HFQ( ()->'abc' ) ) );
// Тип отсутствует, потому что HPQ возвращает nil
OtpObject( Context.Default.SyncInvoke( HPQ( ()->Writeln('Выполнилась HPQ') ) ) );
end.
После выполнения очереди метод Context.SyncInvoke
возвращает то, что вернула очередь.
А если использовать метод Context.BeginInvoke
- возвращаемое значение можно получить с помощью метода CLTask.GetRes
.
Бывает необходимо хранить несколько очередей, с разными возвращаемыми значениями,
вместе, к примеру, в переменной типа List<>
.
Однако в переменной типа CommandQueue<SomeT>
можно хранить только очередь с конкретным типом SomeT
.
Для того чтоб хранить очереди с любым возвращаемым значением в 1 переменной - надо использовать CommandQueueBase
.
CommandQueueBase
это особый тип очереди, у которого не указывается возвращаемое значение.
От него наследует CommandQueue<>
, поэтому переменной типа CommandQueueBase
можно присвоить любую очередь.
Если попытаться выполнить такую очередь в Context.[Begin,Sync]Invoke
,
или применять операции преобразования очередей, как .ThenConvert
,
то тип возвращаемого значения будет восприниматься как Object
.
Есть всего 7 базовых способов создать очередь:
Самый просто способ создать очередь — выбрать объект типа Buffer
или Kernel
и вызвать для него метод .NewQueue
.
Полученная очередь будет иметь особый тип: BufferCommandQueue
/KernelCommandQueue
для буфера/kernel'а соответственно.
К такой очереди можно добавлять команды, вызывая её методы, имена которых начинаются с .Add...
.
К примеру:
uses OpenCLABC;
begin
// Буфер достаточного размера чтоб содержать 3 значения типа integer
var b := new Buffer( 3*sizeof(integer) );
// Создаём очередь
var q := b.NewQueue;
// Добавлять команды в полученную очередь можно вызывая соответствующие методы
q.AddWriteValue(1, 0*sizeof(integer) );
// Методы, добавляющие команду в очередь - возвращают очередь, для которой их вызвали (не копию а ссылку на оригинал)
// Поэтому можно добавлять по несколько команд в 1 строчке:
q.AddWriteValue(5, 1*sizeof(integer) ).AddWriteValue(7, 2*sizeof(integer) );
// Все команды в q будут выполнятся последовательно, что не всегда хорошо
// Если надо выполнять параллельно - создавайте несколько "b.NewQueue" и умножайте друг на друга
// В данной версии надо писать "as CommandQueue<...>" при использовании [Buffer/Kernel]CommandQueue там,
// где принимает CommandQueue<...>, из за бага компилятора #1981
Context.Default.SyncInvoke(q as CommandQueue<Buffer>);
// Вообще чтение тоже надо делать через очереди, но для простого примера - и неявные очереди подходят
b.GetArray1&<integer>(3).Println;
end.
Так же, очереди BufferCommandQueue
/KernelCommandQueue
можно создавать из очередей, возвращающих Buffer
/Kernel
соответственно. Для этого используется конструктор:
var q0: CommandQueue<Buffer>;
...
var q := new BufferCommandQueue(q0);
Переменной очереди можно присвоить значение, тип которого совпадает с возвращаемым значением очереди:
var q: CommandQueue<integer> := 5;
Этот код присваевает переменной q
константную очередь, которая ничего не выполняет и возвращает 5
.
Получить значение, из которого создали константную очередь, можно преобразовав её к ConstQueue<>
:
var cq := q as ConstQueue<integer>;
if cq=nil then
Writeln('Очередь не константная') else
Writeln($'Очередь была создана из значения {cq.Val}');
CommandQueueBase
так же можно создать из значения, но для этого - тип значения должен быть Object
:
var q: CommandQueueBase := 5 as object;
Чтоб получить значение, из которого создали константную очередь, когда не знаете его тип - используйте интерфейс IConstQueue
:
var cq := q as IConstQueue;
if cq=nil then
Writeln('Очередь не константная') else
Writeln($'Очередь была создана из значения ({cq.GetConstVal})');
Иногда между командами для GPU надо вставить выполнение обычного кода на CPU. А разрывать для этого очередь на две части - плохо, потому что одна целая очередь всегда выполнится быстрее двух её частей.
Для таких случае существуют глобальные подпрограммы HFQ и HPQ:
HFQ — Host Function Queue
HPQ — Host Procedure Queue
(Хост в контексте OpenCL - это CPU, потому что с него посылаются команды для GPU)
Они возвращают очередь, выполняющую код (функцию/процедуру соотвественно) на CPU.
Подробнее уже описано
на предыдущей странице
.
Если сложить две очереди A и B (var C := A+B
) — получится очередь C, в которой сначала выполнится A, а затем B.
Очередь C будет считаться выполненной тогда, когда выполнится очередь B.
Если умножить две очереди A и B (var C := A*B
) — получится очередь C, в которой одновременно начнут выполняться A и B.
Очередь C будет считаться выполненной тогда, когда обе очереди (A и B) выполнятся.
Как и в математике, умножение имеет бОльший приоритет чем сложение.
В обоих случаях очередь C будет возвращать то, что вернула очередь B. То есть если складывать и умножать много очередей - результат будет всегда возвращать то, что вернула самая последняя очередь.
Простейший пример:
uses OpenCLABC;
begin
var q1 := HPQ(()->
begin
// lock надо чтоб при параллельном выполнении два потока не пытались использовать вывод одновременно. Иначе выведет кашу
lock output do Writeln('Очередь 1 начала выполняться');
Sleep(500);
lock output do Writeln('Очередь 1 закончила выполняться');
end);
var q2 := HPQ(()->
begin
lock output do Writeln('Очередь 2 начала выполняться');
Sleep(500);
lock output do Writeln('Очередь 2 закончила выполняться');
end);
Writeln('Последовательное выполнение:');
Context.Default.SyncInvoke( q1 + q2 );
Writeln;
Writeln('Параллельное выполнение:');
Context.Default.SyncInvoke( q1 * q2 );
end.
Операторы += и *= также применимы к очередям.
И как и для чисел - A += B
работает как A := A+B
(и так же с *=).
А значит возвращаемые типы очередей A и B должны быть одинаковыми, чтобы к ним можно было применить +=/*=.
Если надо сложить/умножить много очередей - лучше применять CombineSyncQueue
/CombineAsyncQueue
соответственно.
Эти подпрограммы работают немного быстрее чем сложение и умножение, если объединять больше двух очередей.
Кроме того они так же могут принимать ещё 1 параметр перед очередями: Этот параметр позволяет указать функцию преобразования, которая использует результаты всех входных очередей:
uses OpenCLABC;
begin
var q1 := HFQ( ()->1 );
var q2 := HFQ( ()->2 );
// Выводит 2, то есть только результат последней очереди
// Так сделано из за вопросов производительности
Context.Default.SyncInvoke( q1+q2 ).Println;
// Однако всё же бывает так, что нужны результаты всех сложенных/умноженных очередей
// В таком случае надо использовать CombineSyncQueue и CombineAsyncQueue
// А точнее их перегрузку, первый параметр которой - функция преобразования
Context.Default.SyncInvoke(
CombineSyncQueue(
results->results.JoinIntoString, // функция преобразования
q1, q2
)
).Println;
// Теперь выводит строку "1 2". Это то же самое что вернёт "Arr(1,2).JoinIntoString"
end.
Если надо с минимальными затратами изменить представление компилятора об очереди - лучше всего использовать .Cast
.
Но он ограничен примерно так же, как метод последовательностей .Cast
. То есть:
type t1 = class ... end;
type t2 = class(t1) ... end;
...
var Q1: CommandQueue<integer> := 5;
var Q2: CommandQueueBase := Q1;
var Q3: CommandQueue<t1> := (new t2) as t1;
var Q4: CommandQueue<t1> := new t1;
var Q5: CommandQueue<t2> := new t2;
// Можно, потому что к object можно преобразовать всё
Context.Default.SyncInvoke( Q1.Cast&<object> );
// Нельзя, преобразование из integer в byte - изменяет внутреннее представление данных
Context.Default.SyncInvoke( Q1.Cast&<byte> );
// Можно, Q2 и так имеет тип CommandQueue<integer>, а значит тут Cast вернёт (Q2 as CommandQueue<integer>)
Context.Default.SyncInvoke( Q2.Cast&<integer> );
// Можно, потому что Q3 возвращает t2
Context.Default.SyncInvoke( Q3.Cast&<t2> );
// Нельзя, Q4 возвращает не t2 а t1, поэтому к t2 преобразовать не получится
Context.Default.SyncInvoke( Q4.Cast&<t2> );
// Можно, потому что t2 наследует от t1
Context.Default.SyncInvoke( Q5.Cast&<t1> );
Ну а если эти ограничения не подходят - остаётся только .ThenConvert
. Он позволяет указать любой алгоритм преобразования, но и требует бОльшей производительности:
uses OpenCLABC;
begin
var q := HFQ(()->123);
Context.Default.SyncInvoke(
q.ThenConvert(i -> i*2 )
).Println;
end.
В данный момент всё ещё не работает... Но уже совсем скоро, правда правда!
Передавать команды по одной, когда их несколько - ужасно медленно!
Но нередко бывает так, что команда всего одна. Или для отладки надо одноразово выполнить одну команду.
Для таких случаев можно создавать очередь неявно:
У каждого метода очереди, создаваемой с .NewQueue
есть дублирующий метод в оригинальном объекте.
Такие методы сами создают новую очередь, добавляют в неё одну соответствующую команду и выполняют полученную очередь в Context.Default.SyncInvoke(...)
.
Следующий пример сравнивает явные и неявные очереди:
uses OpenCLABC;
begin
var b := new Buffer( 3*sizeof(integer) );
var A := new integer[3];
// Код с очередями
var Q_BuffWrite :=
( b.NewQueue.AddWriteValue(1, 0*sizeof(integer) ) as CommandQueue<Buffer> ) *
( b.NewQueue.AddWriteValue(5, 1*sizeof(integer) ) as CommandQueue<Buffer> ) *
( b.NewQueue.AddWriteValue(7, 2*sizeof(integer) ) as CommandQueue<Buffer> )
;
var Q_BuffRead := b.NewQueue.AddReadArray(A) as CommandQueue<Buffer>;
var Q_Otp := HPQ(()->
begin
A.Println;
end);
Context.Default.SyncInvoke(
Q_BuffWrite +
Q_BuffRead +
Q_Otp
);
// Этот же код ещё раз, но без явных очередей
// Неявно - каждый метод .Write*** и .Read*** всё равно создаёт по очереди
// Такая запись короче, но выполняется медленнее
// Аналог Q_BuffWrite
System.Threading.Tasks.Parallel.Invoke(
()->b.WriteValue(1, 0*sizeof(integer) ),
()->b.WriteValue(5, 1*sizeof(integer) ),
()->b.WriteValue(7, 2*sizeof(integer) )
);
// Аналог Q_BuffRead
b.ReadArray(A);
// Аналог Q_Otp
A.Println;
end.
Кроме того, у типа Buffer
есть дополнительные методы Buffer.Get...
.
Соответствующих методов у очередей — нет (ToDo
возможно, в будущем появятся).
Методы .Get...
создают новый объект типа записи, массива или выделяют область неуправляемой памяти,
читают в полученный объект содержимое буфера и возвращают этот объект.
Они также используют неявную очередь (для чтения буфера).
Самый простой способ выполнить очередь - вызвать метод Context.SyncInvoke
.
Он синхронно выполняет очередь и вызвращает её результат.
Но если надо выполнить очередь асинхронно - лучше использовать метод Context.BeginInvoke
.
Он запускает асинхронное выполнение очереди и как только очередь была полностью запущена - возвращает объект типа CLTask<>
, через который можно:
CLTask<>.Wait
.CLTask<>.GetRes
.Метод Context.SyncInvoke
реализован через .BeginInvoke(...).GetRes
. Поэтому, далее всюду где сказано "... происходит при вызове .BeginInvoke
" - это же относится и к .SyncInvoke
.
У CLTask
как и у очереди - в <>
указывается тип возвращаемого значения. То есть:
var t: CLTask<integer>;
В такую переменную можно сохранить только результат Context.BeginInvoke
для очереди типа CommandQueue<integer>
.
Если при выполнении возникла ошибку, о ней выведет не полную информацию. Чтобы получить всю информацию - используется try
:
try
//ToDo ваш код, вызывающий ошибку
except
// Writeln выводит все внутренние исключения. "e.ToString" тоже.
on e: Exception do Writeln(e);
end;
Для этого кода есть стандартный снипет. Чтоб активировать его - напишите tryo
и нажмите Shift+Пробел.
Все методы создающие одну команду (.Add*
методы + все методы неявных очередей)
могут принемать очередь вместо значения в качестве любого параметра. Но в таком случае
возвращаемый тип очереди должен совпадать с типом параметра. К примеру:
uses OpenCLABC;
begin
var b := new Buffer(10*sizeof(integer));
// Очищаем весь буфер ноликами, чтоб небыло мусора
b.FillValue(0);
var q := b.NewQueue
// Второй параметр AddWriteValue - отступ от начала буфера
// Он имеет тип integer, а значит можно передать и CommandQueue<integer>
// Таким образом в параметр сохраняется алгоритм а не готовое значение
// Поэтому 3 вызова ниже могут получится с 3 разными отступами
.AddWriteValue(5, HFQ(()-> Random(0,9)*sizeof(integer) ))
as CommandQueue<Buffer>;
Context.Default.SyncInvoke(q);
Context.Default.SyncInvoke(q);
Context.Default.SyncInvoke(q);
b.GetArray1&<integer>.Println;
end.
Все очереди-параметры начинают выполнятся сразу при вызове метода Context.BeginInvoke
, не ожидая других очередей.
Обычно очереди-параметры используются при вызове kernel'а, когда надо записать что то в буфер прямо перед вызовом kernel'а.
ToDo
эта страница будет переделана и перенесена на страницу создания очередей, потому что необходимость в .Clone скоро отпадёт и большинство текста тут станет устаревшим// 3.4.0
//
// Одну и ту же очередь можно использовать несколько раз:
// <------------------------->
// var Q1: CommandQueue<...>;
// ...
// Context.Default.SyncInvoke(Q1);
// Context.Default.SyncInvoke(Q1);
// <------------------------->
//
// Однако, во время выполнения очередь хранит в себе данные о своём состоянии выполнения и результат, когда он уже вычислен
// Это значит, что 1 объект очереди нельзя выполнять в 2 местах параллельно
// Иначе данные о состоянии и результате двух выполнений перемешаются
// То есть, такой код:
// <------------------------->
// var Q1: CommandQueue<...>;
// ...
// Context.Default.SyncInvoke( Q1 * Q1 );
// <------------------------->
// Приведёт к вызову исключения QueueDoubleInvokeException
//
// Однако, если вам всё же надо использовать одну очередь в нескольких местах одновременно - есть 2 способа:
// 3.4.1 — Клонирование очередей (.Clone)
//
// Методом .Clone можно создать полную копию очереди
// При этом, если исходная очередь проводила какие то вычисления,
// они будут произведены дважды, оригиналом и копией, при вызове обоих
//
// Клон очереди является полностью независимым объектом
// Его можно вызывать не только параллельно с оригиналом
// Оригинал и копию можно вызывать даже в двух разных Context.BeginInvoke одновременно
//
// 3.4.2 — Удлинители для очередей (.Multiusable)
//
// Вариант выполнять очередь несколько раз, как в случае с .Clone, нередко не подходит,
// потому что это удваивает затраты производительности
// И некоторые очереди (например, выполнения kernel'ов) могут дать разные результаты, если выполнить их лишний раз
//
// Если вам нужно использовать результат одной очереди многократно, лучше использовать метод .Multiusable
// !! Созданные таким образом очереди НЕ является независимыми объектами !!
//
// .Multiusable работает словно провод-удлинитель,
// если сравнивать возвращаемое значение очереди с розеткой
//
// Исходная очередь, для которой вызвали .Multiusable начинает выполняться сразу при вызове Context.BeginInvoke,
// Если хотя бы 1 из очередей, которую вернул метод .Multiusable была использована в этом Context.BeginInvoke
//
// Очереди, которые вернул .Multiusable могут быть использованы параллельно:
// <------------------------->
// var Q1: CommandQueue<...>;
// ...
// Qs1 := Q1.Multiusable(3); // создаёт массив из 3 очередей
// Context.Default.SyncInvoke( Qs1[0] * Qs1[1] * Qs1[2].ThenConvert(o->...) );
// <------------------------->
// Однако, все очереди, полученные из .Multiusable всё ещё связаны оригинальной очередью
// А так как Context.BeginInvoke управляет переключением состояния выполнения очереди
// Следующий код попытается два раза запустить очередь Q1:
// <------------------------->
// var Q1: CommandQueue<...>;
// ...
// Qs1 := Q1.Multiusable(2);
// Context.Default.BeginInvoke( Qs1[0] );
// Context.Default.BeginInvoke( Qs1[1] );
// <------------------------->
// А т.к. это запрещено - будет вызвано исключение QueueDoubleInvokeException
// Чтобы исправить такой код - надо объеденить вызовы метода Context.BeginInvoke
//
Между командами для GPU (хранимыми в очередях типов BufferCommandQueue
и KernelCommandQueue
)
бывает надо вставить выполнение другой очереди или кода для CPU.
Это можно сделать, используя несколько .NewQueue
:
var b: Buffer;
var q0: CommandQueueBase;
...
var q :=
b.NewQueue.AddWriteValue(...) +
q0 +
HPQ(...) +
b.NewQueue.AddWriteValue(...)
;
Однако можно сделать и красивее:
var b: Buffer;
var q0: CommandQueueBase;
...
var q := b.NewQueue
.AddWriteValue(...)
.AddQueue(q0)
.AddProc(...)
.AddWriteValue(...)
;
Эти методы не имеют незаменимых применений, но позволяют сделать код значительно читабельнее
Программы, выпоняемые на GPU, не могут использовать оперативную память (без определённых расширений). Поэтому приходится выделять память на самом GPU.
Объекты типа Buffer
содержат информацию об области памяти,
выделенной на GPU, а так же предоставляют методы для работы с этой памятью.
Буфер создаётся конструктором (new Buffer(...)
).
Но если не передавать контекст в конструктор - память на GPU выделится только при вызове метода Buffer.Init
.
Если вызвать Buffer.Init
2 раза - память освободится и выделится заново.
Когда на GPU выделяется память - она НЕ отчищается нулями, а значит содержит мусорные данные.
Если метод Buffer.Init
не был вызван до первой операции чтения/записи - он будет вызван автоматически.
В таком случае в качестве контекста в котором выделяется память берётся тот,
для которого вызвали .BeginInvoke
, который запустил команду чтения/записи буфера.
Память на GPU можно моментально освободить, вызвав метод Buffer.Dispose
.
Но если снова использовать буфер, у которого освободили память - память опять выделится.
Если сборщик мусора удаляет объект типа Buffer
- .Dispose
вызывается автоматически.
Обычные программы невозможно запустить на GPU. Для этого надо писать особые программы.
В контексте OpenCL - эти программы обычно пишутся на языке "OpenCL C" (основанном на языке "C").
Язык OpenCL-C это часть библиотеки OpenCL, поэтому его справку можно найти там же где и справку OpenCL.
В OpenCLABC
код на языке "OpenCL C" хранится в объектах типа ProgramCode
.
Объекты этого типа используются только как контейнеры.
Один объект ProgramCode может содержать любое количествово подпрограмм-kernel-ов.
ProgramCode
:Конструктор ProgramCode
(new ProgramCode(...)
) принимает тексты исходников программы на языке OpenCL-C.
Именно тексты исходников, не имена файлов
Так же как исходники паскаля хранятся в .pas файлах - исходники OpenCL-C кода обычно хранят в .cl файлах.
Но вообще это не принципиально, потому что код даже не обязательно должен быть в файле.
Так как конструктор ProgramCode
принимает текст - исходники программы на OpenCL-C можно хранить даже в строке в .pas программе.
Тем не менее, хранение исходников OpenCL-C кода в .cl файлах упрощает жизнь, потому что тогда их легко найти.
ToDo
в данный момент наблюдаются некоторые проблемы, возможно ProgramCode.DeserializeFrom
реализован неправильно.После создания объекта типа ProgramCode
из исходников можно вызвать
метод ProgramCode.SerializeTo
, чтобы сохранить код в бинарном и прекомпилированном виде.
Обычно это делается отдельной программой (не той же самой, которая будет использовать этот бинарный код).
После этого основная программа может создать объект ProgramCode
,
используя статический метод ProgramCode.DeserializeFrom
.
К примеру, имеем следующий файл 0.cl
:
__kernel void TEST(__global int* message)
{
int gid = get_global_id(0);
message[gid] += gid;
}
Для начала используем такую программу чтоб прекомпилировать его в бинарный файл:
uses OpenCLABC;
begin
var code := new ProgramCode(ReadAllText('0.cl'));
var nf := System.IO.File.Create('0.bin');
code.SerializeTo(nf);
nf.Close;
end.
Затем готовый бинарный файл можно загрузить так:
uses OpenCLABC;
begin
{$resource '0.bin'} // добавляет 0.bin внутрь готового .exe файла
var code := ProgramCode.DeserializeFrom(Context.Default, GetResourceStream('0.bin'));
var A := new Buffer( 10 * sizeof(integer) );
code['TEST'].Exec1(10,
A.NewQueue.AddFillValue(1)
as CommandQueue<Buffer>
);
A.GetArray1&<integer>(10).Println;
end.
Объект типа Kernel
представляет одну подпрограмму в OpenCL-C коде,
объявленную с ключевым словом __kernel
.
Kernel
создаётся через индексное свойтсво ProgramCode
:
var k := code['KernelName'];
Где code
имеет тип ProgramCode
, а 'KernelName'
— имя подпрограммы-kernel'а в исходном коде (регистр важен!).
Пример выполнения kernel-а:
uses OpenCLABC;
begin
// проще всего - прямую считать текст исходника из файла:
// var code_text := ReadAllText('0.cl');
// но лучше добавить .cl файл внутрь .exe и загружать оттуда:
{$resource '0.cl'}
var code_text := System.IO.StreamReader.Create( GetResourceStream('0.cl') ).ReadToEnd;
// так не нужно таскать .cl файл вместе с .exe
var code := new ProgramCode(code_text);
var A := new Buffer( 10 * sizeof(integer) ); // буфер на 10 чисел типа "integer"
// 'TEST' - имя подпрограммы-кёрнела из .cl файла. Регистр важен!
var kernel := code['TEST'];
kernel.Exec1(10, // используем 10 потоков
A.NewQueue.AddFillValue(1) // заполняем весь буфер единичками, прямо перед выполнением
as CommandQueue<Buffer> //ToDo нужно только из за issue компилятора #1981, иначе получаем странную ошибку. Когда исправят - можно будет убрать
);
A.GetArray1&<integer>.Println; // читаем весь буфер как одномерный массив с элементами типа "integer" и сразу выводим
end.
Файл 0.cl
:
__kernel void TEST(__global int* message)
{
int gid = get_global_id(0);
message[gid] += gid;
}