#Окружение

Окружение Scope представляет собой node.js, к которому подключен пропатченный нами нативный аддон node-cuda, обеспечивающий возможность компилировать и запускать CUDA код из JavaScript

Для того, чтобы запуск CUDA кода был возможен, необходимо наличие в системе

На данный момент, node-cuda загружен в репозиторий Scope как виде исходников, так и в скомпилированном виде.

Для быстрого старта нужно использовать скомпилированную версию, в этом случае нужно, чтобы в системе был CUDA фреймворк именно версии 7.0 и node.js именно версии 4.4.0 32bit.

Если версии этого ПО будут отличаться, нужно будет перепомилировать node-cuda

#element.js

Над node-cuda реализован вспомогательный фреймворк element.js, на котором, в свою очередь, построен фреймворк Scope

element.js представляет собой фреймворк предыдущего поколения, на котором велась разработка симуляций до появления Scope

Практически вся работа с node-cuda идёт не напрямую, а через element.js

Можно сказать, что Scope является развитием парадигмы element.js, а также использует element.js как технический базис для своей реализации.

На текущий момент, знание особенностей element.js может понадобиться только при необходимости “похачить” возможности Scope фреймворка.

Технически, каждый Scope содержит соответствующий ему экземпляр element.js:

var scope = Scope({ ... });
scope.el // элемент, в который спроецирован данный Scope

Документация по element.js пока отсутствует.

#use

в Scope окружении повсеместно используется модуль use.

Этот модуль аналогичен по функциональности модулю require, но производит поиск модулей, указанных без пути к ним, не в node_modules папках, а в корне каждой директории, начиная от текущей и вверх вплоть до корня проекта, определяемого по наличию файла .project_root, либо до корня файловой системы.

Такое поведение организовано для возможности кастомизировать поведение библиотек окружения, копируя и модифицируя некоторые из них в папки отдельных симуляций:

Например, если необходимо быстро кастомизировать ренедеринг, можно скопировать render.scope из корня репозитория в папку создаваемой симуляции, и конструкция js Render: use('render') начнёт подхватывать локальную копию библиотеки render, вместо глобальной. Код симуляции при этом менять не придётся.

Кроме того, use расширяет множество дефолтных расширений:

*.js    // обычные js файлы
*.node  // скомпилированные (бинарные) нативные аддоны node.js
*.scope // модули, которые могут быть напрямую подключены в Scope дерево
*.mixin // зарезервированное расширение

#node-void

node-void - это корневой модуль окружения, который производит несколько вещей:

*.void
*.mixin
*.scope

( внимание, *.js файлы НЕ обрабатываются )

хук препарсит подключаемый исходный код файла, обнаруживает все сочетания символов `) и добавляет в каждый такой найденный фрагмент кода, случайное число

это необходимо для функционирования системы кеширования инлайновых вставок Scope кода:

this(`
	// scope kernel code
`);

преобразуется в

this(`
	// scope kernel code
`, 0.12834619521816211 );

при каждом запуске симуляции эти числа разные, но в пределах одного запуска повторные вполнения одних и тех же участков кода позволяют системе кеширования Scope идентифицировать по этим числам точку кода запуска инлайнового kernel’а, обеспечивая однократную компиляцию и последующее высокоэффективное многократное выполнение без повторных попыток компиляции

*.js файлы не обрабатываются, чтобы случайно не сломать лишними параметрами код какой-либо внешней библиотеки, содержащий ту же последовательность символов `)

изначально, void окружение было организовано не в node.js, а в node-webkit. это позволяло использовать webgl и систему окон chromium для рендеринга и организации пользовательского интерфейса.

запуск Scope в node-webkit окружении всё ещё возможен, но не может обеспечить максимальный FPS, за счёт невозможности использовать CUDA VBO (возможность использовать для вычислений и рендеринга одну и ту же память на GPU)

весной 2016 года мы перешли на node.js + связку модулей, обеспечивающих webgl контекст в отдельном окне, без DOM и других возможностей chromium:

при инициализации происходит последовательное подключение

- node-cuda  // пропатченный аддон для работы с CUDA
- node-webgl // пропатченный аддон для организации webgl контекста
- node-glfw  // пропатченный аддон для создания glfw окна и обработки событий ресайза окна, клавиатуры и мыши

затем создаётся окно с webgl конеткстом, подключается three.js и управление передаётся непосредственно *.void стартовому файлу

#*.void

файлы с расширением *.void являются стартовыми файлами Scope симуляций.

подразумевается, что запуск симуляции будет производится путём ассоциации расширения *.void с void/void.bat обработчиком. но это пока не работает, поэтому рядом с каждым *.void файлом приходится иметь run.js файл, который может запустить данный *.void файл:

global.VOID_MAIN_FILEPATH = __dirname + __filename;
require('node-void');
use    ('run.void'); // этот use будет обработан хуком

почему стартовым файлом не является обычный *.js файл, который можно будетпросто проассоциировать с node.js, либо запускать как node run.js ?

основная причина - в необходимости обработать хуком содержимое стартового файла run.js, чтобы инлайновые Scope-kernel’ы получили свои уникальные random-метки.

ВАЖНО:
если кто-то придумает такой require('node-void'),
который можно будет написать первой строчкой в run.js,
так, чтобы в результате код run.js перед выполнением обработался хуком,
описанным в node-void - это будет очень хорошо и мы сможем вообще уйти от *.void расширения

подразумевается, что папка каждой симуляции будет иметь примерно такую стркутуру:

run.void
module1.scope
module2.scope
big_module3/index.scope
big_module4/index.scope

содержимое файла run.void обычно такое:

( для запуска этого примера, см. samples/01_simple )

// подключаем Scope
var Scope = use('scope');
// подключаем event loop
var loop  = use('loop');

// создаём корневой Scope
Scope({
	// подключаем render.scope (библиотека рендеринга)
	Render: use('render'),
	// подключаем random.scope (GPU-side генератор случайных чисел)
	Random: use('random'),
	
	// описываем Point
	Point: {
		// каждый Point имеет свои координаты
		pos: 'vbo float2',
		// цвет
		rgb: 'vbo float3',
		// вектор движения
		vel: 'float2',
		
		// go() будет рекурсивно вызван при запуске для каждого Scope
		go() {
			// Point регистрирует себя как облако частиц в библиотеке render.scope
			this.Render.pCloud([this, 'pos', 'rgb']);
		}
	},
	
	// корневой go() - это аналог функции main(), с него начинается выполение симуляции
	// (сначала происходит подключение и инициаизация всех подключенных и описанных sub Scope'ов )
	go() {
		var me = this;
		
		// на этапе инициализации - создаём 100000 point'ов
		// запускаем параллельный for ( int i = 0; i < 100000; i++ )
		this(100000, `
			// создаём Point
			Point.new {
				// задаём стартовые координаты
				.pos = ri2xy(1, i);
				// стартовый вектор движения
				.vel = rt2xy( Random.get(), Random.get() ) * 0.1;
				// цвет
				.rgb = t2rgb(i / (float)n);
			};
		`);
		
		// последним действием go() - запускаем симуляцию, запуская event loop
		loop({
			// animate фаза - описывает действия, обновляющие модель
			animate(){
				// изменяем координаты всех Point, прибавляя к ней вектор движения
				me.Point(` .pos += .vel; `);
			},
			
			// render  фаза - описывает действия, визуализирующие модель
			render(){
				me.Render.animate();
			},
		});
	},
// завершая описание корневого Scope, запускаем его, вызывая метод go()

// вызов .go() вызовет go()-инициализацию всех вложенных Scope,
// затем, в последнюю очередь, вызов .go() для корневого Scope
}).go();

#render.scope

render.scope - модуль, обеспечивающий рендеринг:

для использования render.scope, нужно подключить библиотеку и зарегистрировать в ней, например, облако частиц:

{
	Render: use('Render'),
	Point: {
		// на данный момент, все свойства, используемые при рендеринге,
		// должны иметь префикс "vbo " при описании типа, иначе будет ошибка
		pos: 'vbo float3',
		rgb: 'vbo float3',
		
		go(){
			// после выполнения этой команды, все существующие экземпляры Point будут отображаться при рендеринге
			// параметры:
			//     scope из которого брать координаты и цвета частиц,
			//     свойство, хранящее координаты частиц
			//     свойство, хранящее цвета частиц
			this.Render.pCloud([this, 'pos', 'rgb']);
		},
	},
};

Технически, render.scope не является частью окружения, а является просто “шаблоном-обёрткой” над Three.js

Для кастомизации ренедринга, нужно скопировать render.scope в папку своей симуляции, после чего напрямую редактировать код render.scope с тем, чтобы кастомизировать шейдеры или расширить функционал библиотеки.

#loop

loop.js обеспечивает обычный event loop, возможно вместо него использовать свои абстракции

var loop = use('loop');

Scope({
	go(){
		// здесь описывается первичная инициализация сцены
		// ...
		
		// запуск событийного цикла.
		// animate и render фаза описываются отдельно
		// для возможности кратно ускорять частоту модели относительно частоты визуализации
		loop({
			animate(){
				// ...
			},
			render(){
				// ...
			},
		});
	};
}).go();

для управления кратностью симуляции нужно указать множитель кратности в качестве первого параметра:

// 4 кадра модели на 1 кадр визуализации
loop(4, {
	animate(){
		// ...
	},
	render(){
		// ...
	},
});

либо, можно прибегнуть к эвристическому подбору соотношений частот симуляции и визуализации, “заказав” минимальный FPS визуализации:

// не снижать FPS визуализации ниже 60
loop( [ 60 ], {
	animate(){
		// ...
	},
	render(){
		// ...
	},
});

в таком случае, loop каждые 5 секунд будет сообщать в консоль достигнутую частоту модели при данном FPS визуализации. этот способ, однако, обеспечивает субьективно странную плавность выполнения симцуляции, и является экспериментальным.

кроме того, loop.js поддерживает старую нотацию вызова без разделения на animate() и render() фазы:

loop( window, () => {
	// фаза анимации
	// ...
	// фаза рендеринга
	// ...
});

на данный момент, большинство симуляций описаны в старой нотации loop()

Примечание: на текущий момент, работа событийного цикла является полностью синхронной, что приводит к блокированию работы всех асинхронных событий, таких как setInterval, setTimeout и network-модулей node.js

фиксится это выполнением requestAnimationFrame(), однако, в сложных симуляциях его выполнение на каждом кадре способно многократно снижать FPS (видимо, в результате слишком частого срабатывания сборщика мусора V8), поэтому сейчас он не используется

возможным компромиссом является вызов requestAnimationFrame() каждые N кадров