459 lines
22 KiB
JavaScript
459 lines
22 KiB
JavaScript
|
"use strict";
|
||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||
|
exports.ShaderCompiler = exports.CompiledShader = void 0;
|
||
|
const child_process = require("child_process");
|
||
|
const fs = require("fs-extra");
|
||
|
const os = require("os");
|
||
|
const path = require("path");
|
||
|
const chokidar = require("chokidar");
|
||
|
const Throttle = require("promise-parallel-throttle");
|
||
|
const GraphicsApi_1 = require("./GraphicsApi");
|
||
|
const Platform_1 = require("./Platform");
|
||
|
const AssetConverter_1 = require("./AssetConverter");
|
||
|
const log = require("./log");
|
||
|
class CompiledShader {
|
||
|
constructor() {
|
||
|
this.files = [];
|
||
|
this.inputs = [];
|
||
|
this.outputs = [];
|
||
|
this.uniforms = [];
|
||
|
this.types = [];
|
||
|
this.noembed = false;
|
||
|
}
|
||
|
}
|
||
|
exports.CompiledShader = CompiledShader;
|
||
|
class ShaderCompiler {
|
||
|
constructor(exporter, platform, compiler, to, temp, builddir, options, shaderMatchers) {
|
||
|
this.exporter = exporter;
|
||
|
if (platform.endsWith('-native'))
|
||
|
platform = platform.substr(0, platform.length - '-native'.length);
|
||
|
if (platform.endsWith('-hl'))
|
||
|
platform = platform.substr(0, platform.length - '-hl'.length);
|
||
|
this.platform = platform;
|
||
|
this.compiler = compiler;
|
||
|
this.type = ShaderCompiler.findType(platform, options);
|
||
|
this.options = options;
|
||
|
this.to = to;
|
||
|
this.temp = temp;
|
||
|
this.builddir = builddir;
|
||
|
this.shaderMatchers = shaderMatchers;
|
||
|
}
|
||
|
close() {
|
||
|
if (this.watcher)
|
||
|
this.watcher.close();
|
||
|
}
|
||
|
static findType(platform, options) {
|
||
|
switch (platform) {
|
||
|
case Platform_1.Platform.Empty:
|
||
|
case Platform_1.Platform.Node:
|
||
|
return 'glsl';
|
||
|
case Platform_1.Platform.Flash:
|
||
|
return 'agal';
|
||
|
case Platform_1.Platform.Android:
|
||
|
if (options.graphics === GraphicsApi_1.GraphicsApi.Vulkan || options.graphics === GraphicsApi_1.GraphicsApi.Default) {
|
||
|
return 'spirv';
|
||
|
}
|
||
|
else if (options.graphics === GraphicsApi_1.GraphicsApi.OpenGL) {
|
||
|
return 'essl';
|
||
|
}
|
||
|
else {
|
||
|
throw new Error('Unsupported shader language.');
|
||
|
}
|
||
|
case Platform_1.Platform.HTML5:
|
||
|
case Platform_1.Platform.DebugHTML5:
|
||
|
case Platform_1.Platform.HTML5Worker:
|
||
|
case Platform_1.Platform.Pi:
|
||
|
return 'essl';
|
||
|
case Platform_1.Platform.tvOS:
|
||
|
case Platform_1.Platform.iOS:
|
||
|
if (options.graphics === GraphicsApi_1.GraphicsApi.Metal || options.graphics === GraphicsApi_1.GraphicsApi.Default) {
|
||
|
return 'metal';
|
||
|
}
|
||
|
else if (options.graphics === GraphicsApi_1.GraphicsApi.OpenGL) {
|
||
|
return 'essl';
|
||
|
}
|
||
|
else {
|
||
|
throw new Error('Unsupported shader language.');
|
||
|
}
|
||
|
case Platform_1.Platform.Windows:
|
||
|
if (options.graphics === GraphicsApi_1.GraphicsApi.Vulkan) {
|
||
|
return 'spirv';
|
||
|
}
|
||
|
else if (options.graphics === GraphicsApi_1.GraphicsApi.OpenGL) {
|
||
|
return 'glsl';
|
||
|
}
|
||
|
else if (options.graphics === GraphicsApi_1.GraphicsApi.Direct3D11 || options.graphics === GraphicsApi_1.GraphicsApi.Direct3D12 || options.graphics === GraphicsApi_1.GraphicsApi.Default) {
|
||
|
return 'd3d11';
|
||
|
}
|
||
|
else if (options.graphics === GraphicsApi_1.GraphicsApi.Direct3D9) {
|
||
|
return 'd3d9';
|
||
|
}
|
||
|
else {
|
||
|
throw new Error('Unsupported shader language.');
|
||
|
}
|
||
|
case Platform_1.Platform.WindowsApp:
|
||
|
return 'd3d11';
|
||
|
case Platform_1.Platform.Xbox360:
|
||
|
case Platform_1.Platform.PlayStation3:
|
||
|
return 'd3d9';
|
||
|
case Platform_1.Platform.Linux:
|
||
|
if (options.graphics === GraphicsApi_1.GraphicsApi.Vulkan || options.graphics === GraphicsApi_1.GraphicsApi.Default) {
|
||
|
return 'spirv';
|
||
|
}
|
||
|
else if (options.graphics === GraphicsApi_1.GraphicsApi.OpenGL) {
|
||
|
return 'glsl';
|
||
|
}
|
||
|
else {
|
||
|
throw new Error('Unsupported shader language.');
|
||
|
}
|
||
|
case Platform_1.Platform.OSX:
|
||
|
if (options.graphics === GraphicsApi_1.GraphicsApi.Metal || options.graphics === GraphicsApi_1.GraphicsApi.Default) {
|
||
|
return 'metal';
|
||
|
}
|
||
|
else if (options.graphics === GraphicsApi_1.GraphicsApi.OpenGL) {
|
||
|
return 'glsl';
|
||
|
}
|
||
|
else {
|
||
|
throw new Error('Unsupported shader language.');
|
||
|
}
|
||
|
case Platform_1.Platform.Krom:
|
||
|
if (options.graphics === GraphicsApi_1.GraphicsApi.Default) {
|
||
|
if (process.platform === 'win32') {
|
||
|
return 'd3d11';
|
||
|
}
|
||
|
else if (process.platform === 'darwin') {
|
||
|
return 'metal';
|
||
|
}
|
||
|
else {
|
||
|
return 'glsl';
|
||
|
}
|
||
|
}
|
||
|
else if (options.graphics === GraphicsApi_1.GraphicsApi.Vulkan) {
|
||
|
return 'spirv';
|
||
|
}
|
||
|
else if (options.graphics === GraphicsApi_1.GraphicsApi.Metal) {
|
||
|
return 'metal';
|
||
|
}
|
||
|
else if (options.graphics === GraphicsApi_1.GraphicsApi.OpenGL) {
|
||
|
return 'glsl';
|
||
|
}
|
||
|
else if (options.graphics === GraphicsApi_1.GraphicsApi.Direct3D11 || options.graphics === GraphicsApi_1.GraphicsApi.Direct3D12) {
|
||
|
return 'd3d11';
|
||
|
}
|
||
|
else if (options.graphics === GraphicsApi_1.GraphicsApi.Direct3D9) {
|
||
|
return 'd3d9';
|
||
|
}
|
||
|
else {
|
||
|
throw new Error('Unsupported shader language.');
|
||
|
}
|
||
|
case Platform_1.Platform.FreeBSD:
|
||
|
return 'glsl';
|
||
|
default:
|
||
|
return platform;
|
||
|
}
|
||
|
}
|
||
|
watch(watch, match, options, recompileAll) {
|
||
|
return new Promise((resolve, reject) => {
|
||
|
let shaders = [];
|
||
|
let ready = false;
|
||
|
this.watcher = chokidar.watch(match, { ignored: /[\/\\]\.(git|DS_Store)/, persistent: watch });
|
||
|
this.watcher.on('add', (filepath) => {
|
||
|
let file = path.parse(filepath);
|
||
|
if (ready) {
|
||
|
switch (file.ext) {
|
||
|
case '.glsl':
|
||
|
if (!file.name.endsWith('.inc') && this.isSupported(file.name)) {
|
||
|
log.info('Compiling ' + file.name);
|
||
|
this.compileShader(filepath, options, recompileAll);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
switch (file.ext) {
|
||
|
case '.glsl':
|
||
|
if (!file.name.endsWith('.inc')) {
|
||
|
shaders.push(filepath);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
if (watch) {
|
||
|
this.watcher.on('change', (filepath) => {
|
||
|
let file = path.parse(filepath);
|
||
|
switch (file.ext) {
|
||
|
case '.glsl':
|
||
|
if (!file.name.endsWith('.inc') && this.isSupported(file.name)) {
|
||
|
log.info('Recompiling ' + file.name);
|
||
|
this.compileShader(filepath, options, recompileAll);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
this.watcher.on('unlink', (file) => {
|
||
|
});
|
||
|
this.watcher.on('ready', async () => {
|
||
|
ready = true;
|
||
|
let compiledShaders = [];
|
||
|
const self = this;
|
||
|
async function compile(shader, index) {
|
||
|
let parsed = path.parse(shader);
|
||
|
if (self.isSupported(shader)) {
|
||
|
log.info('Compiling shader ' + (index + 1) + ' of ' + shaders.length + ' (' + parsed.base + ').');
|
||
|
let compiledShader = null;
|
||
|
try {
|
||
|
compiledShader = await self.compileShader(shader, options, recompileAll);
|
||
|
}
|
||
|
catch (error) {
|
||
|
log.error('Compiling shader ' + (index + 1) + ' of ' + shaders.length + ' (' + parsed.base + ') failed:');
|
||
|
log.error(error);
|
||
|
return Promise.reject(error);
|
||
|
}
|
||
|
if (compiledShader === null) {
|
||
|
compiledShader = new CompiledShader();
|
||
|
compiledShader.noembed = options.noembed;
|
||
|
// mark variables as invalid, so they are loaded from previous compilation
|
||
|
compiledShader.files = null;
|
||
|
compiledShader.inputs = null;
|
||
|
compiledShader.outputs = null;
|
||
|
compiledShader.uniforms = null;
|
||
|
compiledShader.types = null;
|
||
|
}
|
||
|
if (compiledShader.files != null && compiledShader.files.length === 0) {
|
||
|
// TODO: Remove when krafix has been recompiled everywhere
|
||
|
compiledShader.files.push(parsed.name + '.' + self.type);
|
||
|
}
|
||
|
compiledShader.name = AssetConverter_1.AssetConverter.createExportInfo(parsed, false, options, self.exporter.options.from).name;
|
||
|
compiledShaders.push(compiledShader);
|
||
|
}
|
||
|
else {
|
||
|
log.info('Skipping shader ' + (index + 1) + ' of ' + shaders.length + ' (' + parsed.base + ').');
|
||
|
}
|
||
|
++index;
|
||
|
return Promise.resolve();
|
||
|
}
|
||
|
if (this.options.parallelAssetConversion !== 0) {
|
||
|
let todo = shaders.map((shader, index) => {
|
||
|
return async () => {
|
||
|
await compile(shader, index);
|
||
|
};
|
||
|
});
|
||
|
let processes = this.options.parallelAssetConversion === -1
|
||
|
? require('os').cpus().length - 1
|
||
|
: this.options.parallelAssetConversion;
|
||
|
await Throttle.all(todo, {
|
||
|
maxInProgress: processes,
|
||
|
});
|
||
|
}
|
||
|
else {
|
||
|
let index = 0;
|
||
|
for (let shader of shaders) {
|
||
|
try {
|
||
|
await compile(shader, index);
|
||
|
}
|
||
|
catch (err) {
|
||
|
reject();
|
||
|
return;
|
||
|
}
|
||
|
index += 1;
|
||
|
}
|
||
|
}
|
||
|
resolve(compiledShaders);
|
||
|
return;
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
async run(watch, recompileAll) {
|
||
|
let shaders = [];
|
||
|
for (let matcher of this.shaderMatchers) {
|
||
|
shaders = shaders.concat(await this.watch(watch, matcher.match, matcher.options, recompileAll));
|
||
|
}
|
||
|
return shaders;
|
||
|
}
|
||
|
isSupported(file) {
|
||
|
if (file.endsWith('.frag.glsl') || file.endsWith('.vert.glsl')) {
|
||
|
return true;
|
||
|
}
|
||
|
return this.type !== 'essl' && this.type !== 'agal';
|
||
|
}
|
||
|
compileShader(file, options, recompile) {
|
||
|
return new Promise((resolve, reject) => {
|
||
|
if (!this.compiler)
|
||
|
reject('No shader compiler found.');
|
||
|
if (this.type === 'none') {
|
||
|
resolve(new CompiledShader());
|
||
|
return;
|
||
|
}
|
||
|
let fileinfo = path.parse(file);
|
||
|
let from = file;
|
||
|
let to = path.join(this.to, fileinfo.name + '.' + this.type);
|
||
|
let temp = to + '.temp';
|
||
|
fs.stat(from, (fromErr, fromStats) => {
|
||
|
fs.stat(to, (toErr, toStats) => {
|
||
|
if (options.noprocessing) {
|
||
|
if (!toStats || toStats.mtime.getTime() < fromStats.mtime.getTime()) {
|
||
|
fs.copySync(from, to, { overwrite: true });
|
||
|
}
|
||
|
let compiledShader = new CompiledShader();
|
||
|
compiledShader.noembed = options.noembed;
|
||
|
resolve(compiledShader);
|
||
|
return;
|
||
|
}
|
||
|
fs.stat(this.compiler, (compErr, compStats) => {
|
||
|
if (!recompile && (fromErr || (!toErr && toStats.mtime.getTime() > fromStats.mtime.getTime() && toStats.mtime.getTime() > compStats.mtime.getTime()))) {
|
||
|
if (fromErr)
|
||
|
log.error('Shader compiler error: ' + fromErr);
|
||
|
resolve(null);
|
||
|
}
|
||
|
else {
|
||
|
if (this.type === 'metal' && this.platform !== Platform_1.Platform.Krom) {
|
||
|
fs.ensureDirSync(path.join(this.builddir, 'Sources'));
|
||
|
let funcname = fileinfo.name;
|
||
|
funcname = funcname.replace(/-/g, '_');
|
||
|
funcname = funcname.replace(/\./g, '_');
|
||
|
funcname += '_main';
|
||
|
fs.writeFileSync(to, '>' + funcname, 'utf8');
|
||
|
to = path.join(this.builddir, 'Sources', fileinfo.name + '.' + this.type);
|
||
|
temp = to;
|
||
|
}
|
||
|
let parameters = [this.type === 'hlsl' ? 'd3d9' : this.type, from, temp, this.temp, this.platform];
|
||
|
if (this.options.shaderversion) {
|
||
|
parameters.push('--version');
|
||
|
parameters.push(this.options.shaderversion);
|
||
|
}
|
||
|
else if (this.platform === Platform_1.Platform.Krom && os.platform() === 'linux') {
|
||
|
parameters.push('--version');
|
||
|
parameters.push('110');
|
||
|
}
|
||
|
if (this.options.glsl2) {
|
||
|
parameters.push('--glsl2');
|
||
|
}
|
||
|
if (this.options.debug) {
|
||
|
parameters.push('--debug');
|
||
|
}
|
||
|
if (options.defines) {
|
||
|
for (let define of options.defines) {
|
||
|
parameters.push('-D' + define);
|
||
|
}
|
||
|
}
|
||
|
if (this.platform === Platform_1.Platform.HTML5 || this.platform === Platform_1.Platform.HTML5Worker || this.platform === Platform_1.Platform.Android) {
|
||
|
parameters.push('--relax');
|
||
|
}
|
||
|
parameters[1] = path.resolve(parameters[1]);
|
||
|
parameters[2] = path.resolve(parameters[2]);
|
||
|
parameters[3] = path.resolve(parameters[3]);
|
||
|
let child = child_process.spawn(this.compiler, parameters);
|
||
|
let errorLine = '';
|
||
|
let newErrorLine = true;
|
||
|
let errorData = false;
|
||
|
let compiledShader = new CompiledShader();
|
||
|
compiledShader.noembed = options.noembed;
|
||
|
function parseData(data) {
|
||
|
data = data.replace(':\\', '#\\'); // Filter out absolute paths on Windows
|
||
|
let parts = data.split(':');
|
||
|
if (parts.length >= 3) {
|
||
|
if (parts[0] === 'uniform') {
|
||
|
compiledShader.uniforms.push({ name: parts[1], type: parts[2] });
|
||
|
}
|
||
|
else if (parts[0] === 'input') {
|
||
|
compiledShader.inputs.push({ name: parts[1], type: parts[2] });
|
||
|
}
|
||
|
else if (parts[0] === 'output') {
|
||
|
compiledShader.outputs.push({ name: parts[1], type: parts[2] });
|
||
|
}
|
||
|
else if (parts[0] === 'type') {
|
||
|
let type = data.substring(data.indexOf(':') + 1);
|
||
|
let name = type.substring(0, type.indexOf(':'));
|
||
|
let typedata = type.substring(type.indexOf(':') + 2);
|
||
|
typedata = typedata.substr(0, typedata.length - 1);
|
||
|
let members = typedata.split(',');
|
||
|
let memberdecls = [];
|
||
|
for (let member of members) {
|
||
|
let memberparts = member.split(':');
|
||
|
memberdecls.push({ type: memberparts[1], name: memberparts[0] });
|
||
|
}
|
||
|
compiledShader.types.push({ name: name, members: memberdecls });
|
||
|
}
|
||
|
}
|
||
|
else if (parts.length >= 2) {
|
||
|
if (parts[0] === 'file') {
|
||
|
const parsed = path.parse(parts[1].replace('#\\', ':\\'));
|
||
|
let name = parsed.name;
|
||
|
if (parsed.ext !== '.temp')
|
||
|
name += parsed.ext;
|
||
|
compiledShader.files.push(name);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
let stdOutString = '';
|
||
|
child.stdout.on('data', (data) => {
|
||
|
stdOutString += data.toString();
|
||
|
});
|
||
|
child.stderr.on('data', (data) => {
|
||
|
let str = data.toString();
|
||
|
for (let char of str) {
|
||
|
if (char === '\n') {
|
||
|
if (errorData) {
|
||
|
parseData(errorLine.trim());
|
||
|
}
|
||
|
else {
|
||
|
log.error(errorLine.trim());
|
||
|
}
|
||
|
errorLine = '';
|
||
|
newErrorLine = true;
|
||
|
errorData = false;
|
||
|
}
|
||
|
else if (newErrorLine && char === '#') {
|
||
|
errorData = true;
|
||
|
newErrorLine = false;
|
||
|
}
|
||
|
else {
|
||
|
errorLine += char;
|
||
|
newErrorLine = false;
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
child.on('close', (code) => {
|
||
|
if (stdOutString) {
|
||
|
if (code === 0) {
|
||
|
log.info(stdOutString);
|
||
|
}
|
||
|
else {
|
||
|
log.error(stdOutString);
|
||
|
}
|
||
|
}
|
||
|
if (errorLine.trim().length > 0) {
|
||
|
if (errorData) {
|
||
|
parseData(errorLine.trim());
|
||
|
}
|
||
|
else {
|
||
|
log.error(errorLine.trim());
|
||
|
}
|
||
|
}
|
||
|
if (code === 0) {
|
||
|
if (this.type !== 'metal' || this.platform === Platform_1.Platform.Krom) {
|
||
|
if (compiledShader.files === null || compiledShader.files.length === 0) {
|
||
|
fs.renameSync(temp, to);
|
||
|
}
|
||
|
for (let file of compiledShader.files) {
|
||
|
fs.renameSync(path.join(this.to, file + '.temp'), path.join(this.to, file));
|
||
|
}
|
||
|
}
|
||
|
resolve(compiledShader);
|
||
|
}
|
||
|
else {
|
||
|
process.exitCode = 1;
|
||
|
reject('Shader compiler error.');
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
exports.ShaderCompiler = ShaderCompiler;
|
||
|
//# sourceMappingURL=ShaderCompiler.js.map
|