Данная справка относится к модулю `OpenCLABC`, входящему в состав стандартных модулей языка `PascalABC.NET`.

Модуль `OpenCLABC` это высокоуровневая оболочка модуля `OpenCL`.
Это значит, что с `OpenCLABC` можно писать гораздо меньше кода в больших и сложных программах, однако такой же уровень микроконтроля как с модулем `OpenCL` недоступен. Например, напрямую управлять `cl_event`'ами в `OpenCLABC` невозможно. Вместо этого надо использовать операции с очередями (например, сложение и умножение очередей)

Справка модуля `OpenCL` отсутствует. Вместо неё смотрите:

- Общий гайд по использованию модулей `OpenGL` и `OpenCL`
- Контейнер справок библиотеки `OpenCL`, на которой основан модуль `OpenCL`

Если в справке или модуле найдена ошибка, или чего-либо не хватает - пишите в issue.

- CPU — Центральное Процессорное Устройство (процессор);

- GPU — Графическое Процессорное Устройство (видеокарта);

- Команда — запрос на выполнение чего-либо. К примеру:

  - Запрос на запуск программы на GPU;
  - Запрос на начало чтения данных из памяти GPU в оперативную память;

  Называть процедуры и функции командами ошибочно!

- Подпрограмма — процедура или функция;

- Метод — особая подпрограмма, вызываемая через экземпляр:

  - К примеру, метод `Context.SyncInvoke` выглядит в коде как `cont.SyncInvoke(...)`, где `cont` — переменная типа `Context`;

Остальные непонятные термины можно найти в справке `PascalABC.NET` или в интернете.

Сам модуль `OpenCLABC` - оболочка модуля `OpenCL`. Это значит, что внутри он использует содержимое `OpenCL`, но при подключении - показывает только свой личный функционал.

Так же множество типов из `OpenCLABC` являются оболочками типов из `OpenCL`.

Тип `CommandQueue` использует тип `cl_command_queue`, но предоставляет очень много не связанного с `cl_command_queue` функционала.

"Простые" типы-оболочки модуля `OpenCLABC` предоставляют только функционал соответствующего типа из модуля `OpenCL`... в более презентабельном виде. Из общего - у таких типов есть:

- Свойство `.Native`, возвращающее внутренний объект из модуля `OpenCL`.
  Если вам не пришлось, по какой-либо причине, использовать `OpenCLABC` и `OpenCL` вместе - это свойство может понадобится только для дебага.

- Свойство `.Properties`, возвращающее объект свойств внутреннего объекта.
  Свойства неуправляемого объекта никак не обрабатываются и не имеют описаний. Но типы этих свойств всё равно преобразуются в управляемые (особо заметно на строках и массивах).

# Список простых типов-оболочек:

- Platform - Платформа, объединяющая несколько совместимых устройств;
- Device - Устройство, поддерживающее OpenCL;
- Context - Контекст выполнения;
- Buffer - Область памяти GPU;
- Kernel - Подпрограмма, выполняемая на GPU;
- ProgramCode - Контейнер kernel'ов.

В списке устройств контекста могут быть только совместимые друг с другом устройства.
Коллекция совместимых устройств называется платформой и хранится в объектах типа `Platform`.

Обычно платформы получают из статического свойства `Platform.All`:

    uses OpenCLABC;

    begin
      var pls := Platform.All;
      pls.PrintLines;
    end.

Обычно устройства получают статическим методом `Device.GetAllFor`:

    uses OpenCLABC;

    begin
      foreach var pl in Platform.All do
      begin
        Writeln(pl);
        var dvcs := Device.GetAllFor(pl, DeviceType.DEVICE_TYPE_ALL);
        if dvcs<>nil then dvcs.PrintLines;
        Writeln('='*30);
      end;
    end.

И в большинстве случаев - это всё что вам понадобится.

Но если где то нужен более тонкий контроль - можно создать несколько виртуальных под-устройств, каждому из которых даётся часть ядер изначального устройства.
Для этого используются методы `.Split*`:

    uses OpenCLABC;

    begin
      var dvc := Context.Default.MainDevice;

      Writeln('Поддерживаемые типы .Spilt-ов:');
      // Пустой список, или DEVICE_PARTITION_BY_COUNTS_LIST_END, если ни 1 не поддерживается
      dvc.Properties.PartitionProperties.PrintLines;
      Writeln('='*30);

      Writeln('Виртуальные устройства, по 1 ядру каждое:');
      // Если упадёт потому что слишком много
      // устройств - пожалуйста, напишите в issue
      dvc.SplitEqually(1).PrintLines;
      Writeln('='*30);

      Writeln('Два устройства, 1 и 2 ядра соответственно:');
      dvc.SplitByCounts(1,2).PrintLines;

    end.

Для отправки команд в GPU необходим контекст (объект типа `Context`):
Он содержит информацию о том, какие устройства будут использоваться для выполнения кода на GPU и хранения содержимого буферов.

Создать контекст можно конструктором (`new Context(...)`).
Контекст можно и не создавать, используя везде свойство `Context.Default`.
Неявные очереди всегда используют `Context.Default`.

Изначально этому свойству присваивается контекст, использующий GPU, если оно имеется, или любое другое устройство, поддерживающее OpenCL, если GPU отсутствует.

Если устройств поддерживающих `OpenCL` нет - `Context.Default` будет `nil`.
Однако такая ситуация на практике невозможна, потому что OpenCL поддерживается практически всеми современными устройствами, занимающимися выводом изображения на экран.
Если `Context.Default = nil` - переустановите графические драйверы.

`Context.Default` можно перезаписывать.
Используйте эту возможность только если во всей программе нужен общий контекст, но не стандартный.

Если ваша программа достаточно сложная чтобы нуждаться в нескольких контекстах - лучше не использовать `Context.Default`. И присвоить ему `nil`, чтобы не использовать случайно (к примеру, неявной очередью).

Программам, выпоняемые на GPU, не удобно использовать оперативную память. Поэтому обычно надо выделять память на самом GPU.
Объекты типа `Buffer` представляют такую область памяти.

Буфер создаётся конструктором (`new Buffer(...)`).

Но если не передавать контекст в конструктор - память на GPU выделится только при вызове метода `Buffer.Init`.
Если вызвать `Buffer.Init` 2 раза - память освободится и выделится заново.

Когда на GPU выделяется память - она НЕ очищается нулями, а значит содержит мусорные данные.

Если метод `Buffer.Init` не был вызван до первой операции чтения/записи - он будет вызван автоматически. В таком случае в качестве контекста в котором выделяется память берётся тот, на котором вызвали `.BeginInvoke`, который запустил команду чтения/записи буфера.

Память на GPU можно моментально освободить, вызвав метод `Buffer.Dispose`. Но если снова использовать буфер, у которого освободили память - память выделится заново.

Если сборщик мусора удаляет объект типа `Buffer` - `.Dispose` вызывается автоматически.

Но кроме выделения памяти GPU - OpenCL так же позволяет выделять память внутри другого буфера.
Для этого используется тип `SubBuffer`:

    uses OpenCLABC;

    begin
      var c := Context.Default;

      // Не обязательно MainDevice, можно взять любое устройство из контекста
      var align := c.MainDevice.Properties.MemBaseAddrAlign;

      var b := new Buffer(align*2, c);
      // size может быть любым, но origin
      // должно быть align*N, где N - целое
      var b1 := new SubBuffer(b, 0, Min(123,align));
      var b2 := new SubBuffer(b, align, align);

      Writeln(b1);
      Writeln(b2);
    end.

Объект типа `Kernel` представляет одну подпрограмму в OpenCL-C коде, объявленную с ключевым словом `__kernel`.

Обычно `Kernel` создаётся через индексное свойтсво `ProgramCode`:

    var code: ProgramCode;
    ...
    var k := code['KernelName'];

Тут `'KernelName'` — имя подпрограммы-kernel'а в исходном коде (регистр важен!).

Так же можно получить список всех kernel'ов объекта `ProgramCode`, методом `ProgramCode.GetAllKernels`:

    var code: ProgramCode;
    ...
    var ks := code.GetAllKernels;
    ks.PrintLines;

Методы, запускающие `Kernel` принимают специальные аргументы типа `KernelArg`, которые передаются в OpenCL-C код.
Экземпляр `KernelArg` может быть создан из нескольких типов значений, а точнее:

    uses OpenCLABC;

    begin
      var k: Kernel;
      var val1 := 3;
      var val2 := 5;

      k.Exec1(1,
        // Передавать можно:

        // Буфер
        new Buffer(1),
        // Очередь возвращающую буфер
        HFQ(()->new Buffer(1)),
        // В том числе BufferCommandQueue
        Buffer.Create(1).NewQueue,

        // Размерное значение
        val1,
        HFQ(()->val1),

        // И указатель на размерное значение
        // (в kernel попадёт само значение, не указатель)
        @val2,
        // Так нельзя, потому что val1 уже захвачена лямбдой из HFQ
    //  @val1,
        // Расширенный набор параметров
        KernelArg.FromPtr(new System.IntPtr(@val2), new System.UIntPtr(sizeof(integer)))

      );
    end.

Обратите внимание, `KernelArg` из указателя на `val2` будет немного эффективнее чем `KernelArg` из самого значения `val2`. Но эту возможность стоит использовать только как тонкую оптимизацию, потому что много чего может пойти не так. Если передавать `@val2` в качестве `KernelArg` - надо знать все тонкости.

Если `@val2` передали вкачестве `KernelArg`:

1. `val2` не может быть глобальной переменной или полем класса:

       uses OpenCLABC;

       type
         t1 = class
           val1 := 1; // Не подходит, потому что поле класса
           static val2 := 2; // И статичных полей это тоже касается
         end;

       var
         val3 := 3; // Глобальные переменные - тоже статические поля
         k: Kernel; // k не важно где объявлена

       procedure p1;
       // Теоретически подходит, но вообще это плохой стиль кода
       var val4 := 4;
       begin

         // А val5 однозначно подходит, потому что объявлена не только в той же
         // подпрограмме, но и прямо перед использованием в k.Exec*
         var val5 := 5;

         k.Exec1(1,

           // Это единственные 2 переменные, которые можно передавать адресом
           @val4,
           @val5

         );

       end;

       begin end.

2. `val2` не должно быть захвачего лямбдой.
   Хотя указатель на `val2` уже можно захватывать:

       uses OpenCLABC;

       begin
         var k: Kernel;
         var val1 := 3;
         var val2 := 5;

         // На val1 всё ещё накладываются все ограничени,
         // когда val1_ptr использована в качестве KernelArg
         // Но к самой val1_ptr эти ограничения не применяются
         var val1_ptr := @val1;

         k.Exec1(1,

           val1,
           HFQ(()->val1_ptr^), // захватили val1_ptr, а не val1

           // val1 нигде не захвачена, поэтому теперь так можно
           @val1,
           val1_ptr // то же самое

         );

       end.

3. Выходить из подпрограммы, где объявили `val2` нельзя, пока `.Exec` не закончит выполнятся. Это так же значит, что возвращать очередь, содержащую `KernelArg` из `@val2` обычно нельзя.
   Но это можно обойти, если объявлять переменную в другой подпрограмме, а дальше передавать только её адрес:

       uses OpenCLABC;

       var k: Kernel; // Вообще лучше передавать параметром в p2

       // Обратите внимание - val принимает var-параметром
       // То есть, p2 принимает адрес val
       // (можно так же принимать указатель, но это не так удобно)
       // Иначе переменная будет копироваться при передаче
       // То есть без var перед параметром - val тут
       // будет новой переменной, объявленной в p2 а не в p1
       function p2(var val: integer): CommandQueueBase;
       begin

         Result := k.NewQueue.AddExec1(1,

           @val

         );

       end;

       procedure p1;
       begin
         var val: integer;

         var q := p2(val);
         // Опять же, q не должна продолжать выпоняться
         // после выхода из p1, потому что тут объявлена val
         Context.Default.SyncInvoke(q);

       end;

       begin
         p1;
       end.

Компилятор не заставит вас следовать этим ограничениям. И программа может даже работать, игнорируя большинство сказанного тут.
Но потом вы добавите что то в совсем другой части программы, или запустите её на другом компьютере и она вдруг начнёт выводить мусорные значения, или падать с ошибками вроде `AccessViolationException`, которые совершенно не объясняют в чём проблема.

Если очень интересно откуда берутся эти ограничения - начните с применения .Net декомпилятора к .exe файлам, получаемым при компиляции программ с данной страницы. Что останется непонятно - можете спрашивать в issue.

Обычные программы невозможно запустить на GPU. Для этого надо писать особые программы.
В контексте OpenCL - эти программы обычно пишутся на языке "OpenCL C" (основанном на языке "C").

Язык OpenCL-C это часть библиотеки OpenCL, поэтому его справку можно найти там же, где и справку OpenCL.

В `OpenCLABC` код на языке "OpenCL C" хранится в объектах типа `ProgramCode`.
Объекты этого типа используются только как контейнеры. Один объект ProgramCode может содержать любое количествово подпрограмм-kernel'ов.

# Есть 2 способа создать объект типа `ProgramCode`:

- Из исходного кода

- Из прекомпилированного кода

Конструктор `ProgramCode` (`new ProgramCode(...)`) принимает тексты исходников программы на языке OpenCL-C.
Именно тексты исходников, не имена файлов!

Так же, как исходники паскаля хранят в .pas файлах, исходники OpenCL-C кода хранят в .cl файлах. Но вообще, это не принципиально, потому что код даже не обязательно должен быть в файле.

Так как конструктор `ProgramCode` принимает текст - исходники программы на OpenCL-C можно хранить даже в строке в .pas программе. Тем не менее, храненить исходники OpenCL-C кода в .cl файлах обычно удобнее всего.

После создания объекта типа `ProgramCode` из исходников можно вызвать метод `ProgramCode.SerializeTo`, чтобы сохранить код в бинарном и прекомпилированном виде. Обычно это делается отдельной программой (не той же самой, которая будет использовать этот бинарный код).

После этого основная программа может создать объект `ProgramCode`, используя статический метод `ProgramCode.DeserializeFrom`.

Пример можно найти в папке `Прекомпиляция ProgramCode` или тут.

Передавать команды для 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                           )  );

      // Тип - 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.WaitRes`.

Бывает необходимо хранить несколько очередей, с разными возвращаемыми значениями, вместе. К примеру, в переменной типа `List<>`.
Но в переменной типа `CommandQueue<SomeT>` можно хранить только очередь с конкретным типом возвращаемого значения `SomeT`.

Для того, чтобы хранить очереди с разными возвращаемыми значениями в одной переменной - используется `CommandQueueBase`.
`CommandQueueBase` это особый тип очереди, у которого не указывается возвращаемое значение.
От него наследует `CommandQueue<>`, поэтому переменной типа `CommandQueueBase` можно присвоить любую очередь.

Если попытаться выполнить такую очередь, или применять операции преобразования очередей, как `.ThenConvert`, то тип возвращаемого значения будет восприниматься как `Object`.

Самый простой способ выполнить очередь - вызвать метод `Context.SyncInvoke`.
Он синхронно выполняет очередь и вызвращает её результат.

Но если надо выполнить очередь асинхронно - лучше использовать метод `Context.BeginInvoke`.
Он запускает асинхронное выполнение очереди и как только очередь была полностью запущена - возвращает объект типа `CLTask<>`, у которого есть:

- Event'ы (но реализованы методами), позволяющие указать что должно выполнятся когда выполнение `CLTask` завершится.
- Свойства, возвращающие оригинальную очередь и контекст выполнения.
- Методы для ожидания окончания выполнения и получения результата очереди.

Метод `Context.SyncInvoke` реализован как `.BeginInvoke(...).WaitRes`. Поэтому, везде где сказано "... происходит при вызове `.BeginInvoke`", это же относится и к `.SyncInvoke`.

У `CLTask`, как и у очереди - в `<>`, указывается тип возвращаемого значения. То есть:

    var t: CLTask<integer>;

В такую переменную можно сохранить только результат `Context.BeginInvoke` для очереди типа `CommandQueue<integer>`.

И как и у `CommandQueue`:

- Существует так же и тип `CLTaskBase`, у которого типа возвращаемого значения не указывается.
- Переменной типа `CLTaskBase` можно присвоить `CLTask` с любым возвращаемым значением.
- `Context.BeginInvoke` для очереди типа `CommandQueueBase` возвращает `CLTaskBase`.

Если при выполнении возникла ошибка, о ней выведет не полную информацию. Чтобы получить всю информацию - используется `try`:

    try

      //ToDo ваш код, вызывающий ошибку

    except
      // Writeln выводит все внутренние исключения. "e.ToString" тоже.
      on e: Exception do Writeln(e);
    end;

Для этого кода есть стандартный снипет. Чтобы активировать его - напишите `tryo` и нажмите Shift+Пробел.

# Есть всего 10 базовых способов создать очередь:

1. Из буфера / kernel'а

2. Из готового результата

3. Из обычной подпрограммы

4. Из нескольких других очередей

5. Из очереди + преобразования результата

6. Из повторения одной очереди

7. Несколько очередей с общей работой

8. Особыми `.Add*` методами

9. Из ожидания других очередей

10. Не пытаясь

Самый просто способ создать очередь — выбрать объект типа `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 будут выполняться последовательно

      Context.Default.SyncInvoke(q);

      // Вообще чтение тоже надо делать через очереди, но для простого примера - и неявные очереди подходят
      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<>`:

    if q is ConstQueue<integer>(var cq) then
      Writeln($'Очередь была создана из значения ({cq.Val})') else
      Writeln($'Очередь не константная');

`CommandQueueBase` также можно создать из возвращаемого значения, но для этого тип значения должен быть `Object`:

    var q: CommandQueueBase := 5 as object;

Чтобы получить значение, из которого создали константную очередь, когда не знаете его тип - используйте интерфейс `IConstQueue`:

    if q is IConstQueue(var cq) then
      Writeln($'Очередь была создана из значения ({cq.GetConstVal})') else
      Writeln($'Очередь не константная');

Иногда между командами для GPU надо вставить выполнение обычного кода на CPU. А разрывать для этого очередь на две части - плохо, потому что одна целая очередь всегда выполнится быстрее двух её частей.

Для таких случае существуют глобальные подпрограммы `HFQ` и `HPQ`:

HFQ — Host Function Queue
HPQ — Host Procedure Queue
(Хост в контексте OpenCL - это CPU, потому что с него посылаются команды для GPU)

Они возвращают очередь, выполняющую код (функцию/процедуру соотвественно) на CPU.
Пример применения приведён на странице выше .

Если во время выполнения очереди возникает какая-либо ошибка - весь пользовательский код, выполняемый на CPU в этой очереди получает `ThreadAbortException`:

    uses OpenCLABC;

    begin
      var t := Context.Default.BeginInvoke(
        HPQ(()->
        begin
          try
            Sleep(1000);
          except
            on e: Exception do lock output do
            begin
              Writeln('Ошибка во время выполнения первого HPQ:');
              Writeln(e);
              Writeln;
            end;
          end;
          // Это никогда не выполнится, потому что
          // ThreadAbortException кидает себя ещё раз в конце try
          Writeln(1);
        end)
      *
        HPQ(()->
        begin
          Sleep(500);
          raise new Exception('abc');
        end)
      );

      try
        t.Wait;
      except
        on e: Exception do lock output do
        begin
          Writeln('Ошибка во время выполнения очереди:');
          Writeln(e);
        end;
      end;

    end.

Исключение `ThreadAbortException` во многом опасно.
Подробнее можно прочитать в справке от microsoft.

А в кратце, если в вашем коде была кинута `ThreadAbortException` - становится очень сложно сказать что-либо о его состоянии.
Даже потокобезопастный код, как вызов `Buffer.Dispose`, может привести к утечкам памяти, если посреди него кинут `ThreadAbortException`.

Считайте получение `ThreadAbortException` критической ошибкой, после которой очень желателен перезапуск всего .exe файла.

Если сложить две очереди 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` соответственно.
Эти подпрограммы работают немного быстрее чем сложение и умножение, если объединять больше двух очередей.

Кроме того, они могут принимать ещё один параметр перед очередями: Этот параметр позволяет указать функцию преобразования, которая использует результаты всех входных очередей:

    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.JoinToString, // Функция преобразования
          q1, q2
        )
      ).Println;
      // Теперь выводит строку "1 2". Это то же самое, что вернёт "Arr(1,2).JoinToString"

    end.

Если надо с минимальными затратами изменить представление компилятора об очереди - лучше всего использовать `.Cast`.
Но он ограничен примерно так же, как метод последовательностей `.Cast`. То есть:

    uses OpenCLABC;

    type t1 = class  end;
    type t2 = class(t1) end;

    begin
      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> );

      // Нельзя, преобразование между 2 записями, как из 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> );
    end.

Ну а если эти ограничения не подходят - остаётся только `.ThenConvert`. Он позволяет указать любой алгоритм преобразования, но работает медленнее:

    uses OpenCLABC;

    begin
      var q := HFQ(()->123);

      Context.Default.SyncInvoke(
        q.ThenConvert(i -> i*2 )
      ).Println;

    end.

В данный момент всё ещё не работает... Но уже совсем скоро, правда-правда!

Одну и ту же очередь можно использовать несколько раз, в том числе одновременно:

    uses OpenCLABC;

    begin
      var Q := HPQ(()->lock output do Writeln('Q выполнилась'));

      var t1 := Context.Default.BeginInvoke(Q);
      var t2 := Context.Default.BeginInvoke(Q*Q);

      t1.Wait;
      t2.Wait;
    end.

Но эта программа выведет "Q выполнилась" три раза, потому что при каждом упоминании - Q запускается ещё раз.

Это не всегда хорошо. К примеру, может быть так что `Q` содержит какой то затратный алгоритм. Или ввод значения с клавиатуры:

    uses OpenCLABC;

    begin
      var Q := HFQ(()->
      begin
        lock input do
          Result := ReadInteger;
      end);

      Context.Default.SyncInvoke(CombineAsyncQueue(
        res->res,
        Q,
        Q.ThenConvert(i->i*i),
        Q.ThenConvert(i->i*i*i)
      )).Println;

    end.

Эта программа запросит три разных значения, что не всегда то что надо.

Чтоб использовать результат одной очереди несколько раз - используется `.Multiusable`:

    uses OpenCLABC;

    begin
      var Q := HFQ(()->
      begin
        lock input do
          Result := ReadInteger;
      end);

      var Qs := Q.Multiusable;

      Context.Default.SyncInvoke(CombineAsyncQueue(
        res->res,
        Qs(),
        Qs().ThenConvert(i->i*i),
        Qs().ThenConvert(i->i*i*i)
      )).Println;

    end.

`.Multiusable` создаёт новую функцию, вызывая которую можно получить любое количество очередей, у которых будет общий результат.

Каждый вызов `.Multiusable` создаёт именно новую функцию.
Это значит, что если использовать результаты двух вызовов `.Multiusable` - исходная очередь выполнится два раза.

`.Multiusable` не работает между вызовами `Context.BeginInvoke`:

    uses OpenCLABC;

    begin
      var Q := HFQ(()->
      begin
        lock input do
          Result := ReadInteger;
      end);

      var Qs := Q.Multiusable;

      Context.Default.SyncInvoke( Qs()                      ).Println;
      Context.Default.SyncInvoke( Qs().ThenConvert(i->i*i)  ).Println;

    end.

Эта программа запросит ввод два раза.

Если контекст у двух очередей общий - лучше объединить вызовы `Context.BeginInvoke`. Так не только `.Multiusable` будет работать, но и выполнение будет в целом быстрее.

А если контекст разный - надо сохранять результат в переменную и использовать `Wait` очереди (подробнее на странице ниже).

Между командами для 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(...)
    ;

(вообще callback AddProc'а принимает исходный буфер/kernel параметром, что делает его более похожим на `.ThenConvert`, чем `HPQ`)

Эти методы не имеют незаменимых применений, но позволяют сделать код значительно читабельнее.

Кроме того, будьте осторожны, защита от дурака для такого случая - отсутствует:

    var q := b.NewQueue;
    q.AddQueue(q);

Все `.Add*` методы добавляют команды в существующую очередь, а не создают новую. Поэтому очередь, созданная предыдущим кодом при попытке выполнения начнёт циклически запускать саму себя.

Есть всего 3 группы подпрограмм, создающих очереди для ожидания других очередей:

1. Глобальные, `WaitFor*`:
   Ничего не делают сами, но блокируют выполнение пока указанные очереди не выполнятся.

2. Особые методы `.Add*` - `.AddWait*`:
   Как и `.AddQueue` и `.AddProc`, `.AddWait(...)` это всего лишь аналог `.AddQueue(WaitFor(...))`, существующий только ради простоты кода.

3. Методы очередей, `.ThenWaitFor*`:
   Так же не имеет незаменимых применений, но ещё полезнее.
   `Q.ThenWaitFor(...)` работает как `Q + WaitFor(...)`, за исключением того, что возвращает результат `Q`.
   То есть, правильнее будет:

       var Qs := Q.Multiusable;
       Result := Qs() + WaitFor(...) + Qs();

   Но это уже даже в одну строчку не напишешь.

В каждой из групп Wait очередей - `*` можно заменить:

1. Ничем: Ожидание одной очереди.
2. На `All`: Ожидание всех указанных очередей.
3. На `Any`: Ожидание любой из указанных очередей (какая раньше выполнится).

То есть, к примеру, `WaitForAny` создаёт очередь, которая сама ничего не выполняет, но ожидает выполнения любой из указанных очередей.

`Wait` очереди делают реализацию некоторых сложных деревьев выполнения очередей возможными.
Примеры можно найти в папке `PABCWork.NET\Samples\OpenCLABC\Wait очереди`.

Но они наиболее полезны благодаря одному особенному свойству:
`Wait` очереди работают даже между вызовами `Context.BeginInvoke`, в отличии от всего остального в `OpenCLABC`.

Это не всегда безопастно:

    Context.Default.BeginInvoke(Q1);
    Context.Default.BeginInvoke(WaitFor(Q1) + Q2);

Проблема этого кода в том, что `Q1` может закончить выполняться ещё до того как `WaitFor(Q1)` начнёт ожидать.

Чтоб такое не происходило - надо всегда запускать ожидающую очередь раньше ожидаемой:

    Context.Default.BeginInvoke(WaitFor(Q1) + Q2);
    Context.Default.BeginInvoke(Q1);

Но, как всегда, лучше, по возможности, объединять вызовы `Context.BeginInvoke`:

    Context.Default.BeginInvoke(
      ( Q1 ) *
      ( WaitFor(Q1) + Q2 )
    );

Все ожидающие очереди начинают ожидать в самом начале вызова `Context.BeginInvoke`, перед тем как очередь начнёт выполнятся. Поэтому если ожидающая и ожидаемая очереди находятся в общем `Context.BeginInvoke` - о их порядке можно не волноваться.

### Множественное ожидание несколько раз выполнившейся очереди

Когда начинает выполняться `Context.BeginInvoke` - для каждой ожидающей очереди в соответствующей ожидаемой очереди создаётся новый счётчик для того, сколько раз ожидаемая очередь была выполнена.

Поэтому можно делать так:

    uses OpenCLABC;

    begin
      var Q1 := HPQ(()->
      begin
        Sleep(1000);
        lock output do Writeln('Выполнилась Q1');
      end);

      var Q2 := HPQ(()->lock output do Writeln('Выполнилась Q2'));
      var Q3 := HPQ(()->lock output do Writeln('Выполнилась Q3'));

      var t1 := Context.Default.BeginInvoke(
        (WaitFor(Q1)+Q2) *
        (WaitFor(Q1)+Q3)
      );
      Context.Default.SyncInvoke(Q1+Q1);

      t1.Wait; // Чтобы вывести ошибки, если возникнут при выполнении
    end.

Ну и, конечно, лучше совместить вызовы `Context.BeginInvoke`, раз контекст общий:

    uses OpenCLABC;

    begin
      var Q1 := HPQ(()->
      begin
        Sleep(1000);
        lock output do Writeln('Выполнилась Q1');
      end);

      var Q2 := HPQ(()->lock output do Writeln('Выполнилась Q2'));
      var Q3 := HPQ(()->lock output do Writeln('Выполнилась Q3'));

      Context.Default.SyncInvoke(
        (Q1+Q1) *
        (WaitFor(Q1)+Q2) *
        (WaitFor(Q1)+Q3)
      );

    end.

Каждое окончание выполнения `Q1` добавляет 1 в счётчик внутри `Q1`.
Каждое окончание ожидания `WaitFor(Q1)` отнимание 1 от того же счётчика.

Будьте осторожны, лишняя `Wait` очередь вызовет зависание:

    uses OpenCLABC;

    begin
      var Q1 := HFQ(()->0);

      var t1 := Context.Default.BeginInvoke(
        WaitFor(Q1) +
        WaitFor(Q1) // второй запуск Q1 никогда не произойдёт, поэтому это зависнет
      );
      Context.Default.SyncInvoke(Q1);

      t1.Wait;
    end.

Cчётчик создаётся для каждой пары [ожидаемой очереди] и [вызова `Context.BeginInvoke` с ожидающей очередью]. То есть это тоже сработает:

    uses OpenCLABC;

    begin
      var Q1 := HPQ(()->
      begin
        Sleep(1000);
        lock output do Writeln('Выполнилась Q1');
      end);

      var Q2 := HPQ(()->lock output do Writeln('Выполнилась Q2'));
      var Q3 := HPQ(()->lock output do Writeln('Выполнилась Q3'));

      var Q4 := HPQ(()->lock output do Writeln('Выполнилась Q4'));
      var Q5 := HPQ(()->lock output do Writeln('Выполнилась Q5'));

      var t1 := Context.Default.BeginInvoke(
        ( WaitFor(Q1)+Q2 ) *
        ( WaitFor(Q1)+Q3 )
      );
      var t2 := Context.Default.BeginInvoke(
        ( WaitFor(Q1)+Q4 ) *
        ( WaitFor(Q1)+Q5 )
      );
      // Каждый вызов Q1 тут - активирует по 1 WaitFor(Q1) в каждом CLTask
      Context.Default.SyncInvoke(Q1+Q1);

      t1.Wait;
      t2.Wait;
    end.

Но каждый `Context.BeginInvoke` для ожидаемой очереди - добавляет в уже существующий счётчик:

    uses OpenCLABC;

    begin
      var Q1 := HPQ(()->
      begin
        Sleep(1000);
        lock output do Writeln('Выполнилась Q1');
      end);

      var Q2 := HPQ(()->lock output do Writeln('Выполнилась Q2'));
      var Q3 := HPQ(()->lock output do Writeln('Выполнилась Q3'));
      var Q4 := HPQ(()->lock output do Writeln('Выполнилась Q4'));

      var t1 := Context.Default.BeginInvoke(
        ( WaitFor(Q1)+Q3 ) *
        ( WaitFor(Q1)+Q4 )
      );
      var t2 := Context.Default.BeginInvoke(WaitFor(Q1)+Q2+Q1);
      Context.Default.SyncInvoke(Q1);

      t1.Wait;
      t2.Wait;
    end.

Передавать команды по одной, когда их несколько - ужасно не эффективно!
Но нередко бывает так, что команда всего одна. Или для отладки надо одноразово выполнить одну команду.

Для таких случаев можно создавать очередь неявно:
У каждого `.Add*` метода есть дублирующий метод в оригинальном объекте. Такие методы сами создают новую очередь, добавляют в неё одну соответствующую команду и выполняют полученную очередь в `Context.Default.SyncInvoke(...)`.

Обычный код с очередями:

    uses OpenCLABC;

    begin
      var b := new Buffer( 3*sizeof(integer) );

      var Q_BuffWrite := b.NewQueue
        .AddWriteValue(1, 0*sizeof(integer) )
        .AddWriteValue(5, 1*sizeof(integer) )
        .AddWriteValue(7, 2*sizeof(integer) )
      ;

      var Q_BuffRead :=
        b.NewQueue.AddGetArray1&<integer>
        .ThenConvert(A->A.Println);
      ;

      Context.Default.SyncInvoke(
        Q_BuffWrite +
        Q_BuffRead
      );

    end.

Он же, но с неявными очередями:

    uses OpenCLABC;

    begin
      var b := new Buffer( 3*sizeof(integer) );

      // Аналог Q_BuffWrite
      b.WriteValue(1, 0*sizeof(integer) );
      b.WriteValue(5, 1*sizeof(integer) );
      b.WriteValue(7, 2*sizeof(integer) );

      // Аналог Q_BuffRead
      b.GetArray1&<integer>.Println;

    end.

Все методы создающие одну команду (`.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'а.

В данной справке в нескольких местах можно встретить утверждения вроде

> Вызовы `Context.BeginInvoke` стоит, по возможности, объединять.

Данный раздел подпробнее объясняет устройство модуля, что делает подобные утверждения более понятными.

Читать его не необходимо для написания работающего кода, но желательно для написания качественного кода.

Но стоит сказать заранее - это не полное объяснение внутренностей модуля. Объясняется только то, что скорее всего окажется нужным. Если хотите ещё более полное понимание - используйте Ctrl+клик в IDE по именам, чтоб смотреть исходный код.

# Страницы:

- Что делает вызов `BeginInvoke`

Кроме самого выполнения очередей - им так же необходима инициализация и финализация.

### Инициализация

Для начала, перед тем как любая из под-очередей в `BeginInvoke` начнёт выполняться - необходимо инициалировать `Wait` очереди. Иначе у ожидаемой очереди всегда будет шанс выполнится до того как ожидающая начнёт ожидать.

Инициализация `Wait` очередей заключается в обходе всего дерева под-очередей. Для каждой `Wait` очереди, ожидающей уникальную очередь, создаётся пара ключ-значения (`CLTask` и счётчик для него) и добавляется обработчик `CLTask.WhenDone` с удалением этой пары.

### Запуск очередей

Тоже заключается в обходе дерева под-очередей.

Но в этот раз очереди по которым прошлись - уже начали выполнятся, не ожидая окончания обхода всего дерева.

Как только этот обход закончен - метод `BeginInvoke` возвращает свой `CLTask` коду, вызвавшему его.

Единственное исключение - если очередь, каким то образом, уже завершила выполняться, к моменту как обход дерева закончился.
Такое возможно, к примеру, если все под-очереди - константные. В таком случае финализация выполняется до выхода из `BeginInvoke`.

### Финализация

В основном заключается в вызове обработчиков события, как `CLTask.WhenDone`. Обработчики созданные `Wait` очередями ничем не особенны, поэтому они тоже выполняются тут.

Основное преимущество объединения вызовов `BeginInvoke` состоит в различии следующих 2 случаев:

    Context.Default.SyncInvoke(A+B);

    Context.Default.SyncInvoke(A);
    Context.Default.SyncInvoke(B);

В первом случае пока выполнится `A` - `B` уже, скорее всего, окажется полностью запущено. А значит как только `A` закончит выполнятся - ход выполнения перейдёт на `B`.

А во втором случае - между окончанием выполнения `A` и запуском `B` - будет произведено множество проверок, а так же выходов/входов в объёмные (что значит JIT их не инлайнит) подпрограммы, как конструктор `CLTask`.

Да, всё это мелочи. Но нулевая задержка всегда лучше ненулевой.

Ну а когда всё же приходится вызывать 2 отдельных `BeginInvoke`, к примеру на 2 разных контекстах - можно использовать `Wait` очереди, чтоб добится того же эффекта:

    c2.BeginInvoke(WaitFor(A)+B);
    c1.BeginInvoke(A);

Внутренние оптимизации `OpenCLABC` делают этот код практически не отличимым по скорости, от `BeginInvoke(A+B)`.

Единственное различие - время инициализации. Потому что `A` не запустится, пока не закончится обход дерева `c2.BeginInvoke`.