diff --git a/.gitignore b/.gitignore index 81f9d04..fb49cda 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +node_modules + # Build artifacts *.o *.exe @@ -131,3 +133,5 @@ Thumbs.db .github/copilot/ CLAUDE.md .aider* +web/barracuda.wasm +web/barracuda.js diff --git a/Makefile b/Makefile index 39f0892..a3192fa 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ CFLAGS = -std=c99 -Wall -Wextra -pedantic -O2 \ -Wdouble-promotion -Wswitch-enum -Wwrite-strings \ -D_FORTIFY_SOURCE=2 -fstack-protector-strong -fPIE $(CF_PROT) \ $(GCC_ONLY) \ - -Isrc -Isrc/fe -Isrc/ir -Isrc/tdf -Isrc/amdgpu -Isrc/tensix -Isrc/nvidia -Isrc/metal -Isrc/intel -Isrc/triton -Isrc/cpu -Isrc/runtime + -Isrc -Isrc/fe -Isrc/ir -Isrc/tdf -Isrc/amdgpu -Isrc/tensix -Isrc/nvidia -Isrc/metal -Isrc/intel -Isrc/triton -Isrc/cpu -Isrc/runtime -Iruntime LDFLAGS = -pie LIBS = -lm # Linux/ELF only: -Wl,-z,relro,-z,now -Wl,-z,noexecstack @@ -92,7 +92,38 @@ src/runtime/%.o: src/runtime/%.c runtime/%.o: runtime/%.c $(CC) $(TCFLAGS) -c $< -o $@ +WASM_OUT_DIR = web +WASM_TARGET = $(WASM_OUT_DIR)/barracuda.js + +wasm: $(SOURCES) + @mkdir -p $(WASM_OUT_DIR) + emcc $(SOURCES) -O3 \ + -Isrc -Isrc/fe -Isrc/ir -Isrc/tdf -Isrc/amdgpu -Isrc/tensix -Isrc/nvidia -Isrc/metal -Isrc/intel -Isrc/triton -Isrc/cpu -Isrc/runtime -Iruntime \ + -s ALLOW_MEMORY_GROWTH=1 \ + -s EXPORTED_RUNTIME_METHODS='["FS","callMain"]' \ + -s INVOKE_RUN=0 \ + -s EXPORT_ES6=0 \ + -o $(WASM_TARGET) + @echo "WASM compilation successful. Output in $(WASM_OUT_DIR)/" + +wasm_test: wasm + ./tests/test_wasm_build.sh + node ./tests/test_wasm_run.js + node ./tests/test_worker.js + node ./tests/test_app.js + +wasm_test_e2e: wasm + @if ! node -e "require('puppeteer')" > /dev/null 2>&1; then \ + echo "Ensure puppeteer is installed via 'npm install puppeteer --no-save' before running this target."; \ + exit 1; \ + fi + node ./tests/test_e2e_web.js + +wasm_serve: wasm + @echo "Starting web server on http://localhost:8000" + @python3 -m http.server 8000 --directory web + clean: rm -f $(OBJECTS) $(TARGET) $(TARGET).exe trunner trunner.exe $(TOBJS) src/runtime/*.o runtime/*.o -.PHONY: all clean test +.PHONY: all clean test wasm wasm_test wasm_serve diff --git a/src/main.c b/src/main.c index a2426d7..d2c74e0 100644 --- a/src/main.c +++ b/src/main.c @@ -179,12 +179,28 @@ static int run_bir_backends(bir_module_t *bir, const backend_cfg_t *cfg) } } if (arc == BC_OK) { - if (cfg->mode_amdgpu_bin) + if (cfg->mode_amdgpu_bin) { amdgpu_emit_elf(amd, cfg->output_file ? cfg->output_file : "a.hsaco"); - else - amdgpu_emit_asm(amd, stdout); - } else { + } else { + FILE *out = stdout; + if (cfg->output_file) { + out = fopen(cfg->output_file, "w"); + if (!out) { + fprintf(stderr, "error: could not open output file %s\n", cfg->output_file); + arc = BC_ERR_IO; + } + } + if (arc == BC_OK) { + amdgpu_emit_asm(amd, out); + if (out != stdout) { + fclose(out); + } + } + } + } + + if (arc != BC_OK) { if (arc != BC_ERR_VERIFY) fprintf(stderr, "error: AMDGPU compilation failed\n"); rc = arc; diff --git a/tests/test_app.js b/tests/test_app.js new file mode 100644 index 0000000..c9124be --- /dev/null +++ b/tests/test_app.js @@ -0,0 +1,142 @@ +/** + * @file test_app.js + * @description Lightweight DOM mock test for app.js to ensure event bindings and state updates work. + */ + +const fs = require('fs'); +const path = require('path'); + +// Basic DOM Mock +class DOMNode { + constructor(tagName) { + this.tagName = tagName; + this.children = []; + this.attributes = {}; + this.events = {}; + this.value = ''; + this.textContent = ''; + this.disabled = false; + this.scrollTop = 0; + this.scrollHeight = 100; + } + + appendChild(child) { + this.children.push(child); + } + + addEventListener(event, callback) { + if (!this.events[event]) this.events[event] = []; + this.events[event].push(callback); + } + + dispatchEvent(eventObj) { + const cbs = this.events[eventObj.type] || []; + for (const cb of cbs) cb(eventObj); + } +} + +const mockDocument = { + elements: {}, + getElementById(id) { + if (!this.elements[id]) { + this.elements[id] = new DOMNode('div'); + } + return this.elements[id]; + }, + createElement(tagName) { + return new DOMNode(tagName); + } +}; + +const mockWindow = new DOMNode('window'); + +// Mock Monaco +const mockMonaco = { + editor: { + create: () => ({ + getValue: () => "mock code", + setValue: () => {}, + getModel: () => ({}) + }), + setModelLanguage: () => {} + } +}; + +global.document = mockDocument; +global.window = mockWindow; +global.monaco = mockMonaco; + +// Create a global require function for the eval context +const mockRequire = (deps, cb) => { + if (cb) cb(); +}; +mockRequire.config = () => {}; +global.require = mockRequire; + +// Mock Worker +class MockWorker { + constructor(script) { + this.script = script; + } + postMessage(msg) { + // Simulate immediate response + if (msg.command === 'compile') { + setTimeout(() => { + this.onmessage({ data: { type: 'compile_result', exitCode: 0, output: 'mock output' } }); + }, 10); + } + } +} +global.Worker = MockWorker; + +// Load app.js +const appJsPath = path.resolve(__dirname, '../web/app.js'); +const appJsCode = fs.readFileSync(appJsPath, 'utf8'); + +// Evaluate app.js +let testCode = appJsCode.replace(/require/g, 'mockRequire'); +testCode = testCode.replace(/let /g, 'var ').replace(/const /g, 'var '); +eval(testCode); + +// Trigger DOMContentLoaded +mockWindow.dispatchEvent({ type: 'DOMContentLoaded' }); + +// Test initial state +const btn = mockDocument.getElementById('compile-btn'); +if (btn.textContent !== 'Loading...') { + console.error("Test failed: Button should be in Loading state initially."); + process.exit(1); +} + +// Simulate Worker Ready +eval("compilerWorker.onmessage({ data: { type: 'ready' } })"); + +if (btn.disabled !== false || btn.textContent !== 'Compile') { + console.error("Test failed: Button should be enabled and say 'Compile' after worker ready."); + process.exit(1); +} + +// Trigger compile +btn.dispatchEvent({ type: 'click' }); + +if (btn.disabled !== true || btn.textContent !== 'Compiling...') { + console.error("Test failed: Button should be disabled and say 'Compiling...' during compile."); + process.exit(1); +} + +// Wait for mock compile result +setTimeout(() => { + if (btn.disabled !== false || btn.textContent !== 'Compile') { + console.error("Test failed: Button should be reset after compile."); + process.exit(1); + } + + const outputView = mockDocument.getElementById('output-view'); + if (outputView.value !== 'mock output') { + console.error("Test failed: Output view did not receive mock output."); + process.exit(1); + } + + console.log("All app.js mock tests passed."); + process.exit(0); +}, 50); diff --git a/tests/test_e2e_web.js b/tests/test_e2e_web.js new file mode 100644 index 0000000..4d1346f --- /dev/null +++ b/tests/test_e2e_web.js @@ -0,0 +1,148 @@ +/** + * @file test_e2e_web.js + * @description End-to-End integration test for the BarraCUDA Web Compiler. + * Spins up a local HTTP server and uses Puppeteer to verify the UI, + * Web Worker, and compilation pipeline end-to-end. + */ + +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +const PORT = 3000; +const WEB_DIR = path.resolve(__dirname, '../web'); + +// 1. Setup a simple static HTTP server +const server = http.createServer((req, res) => { + let filePath = path.join(WEB_DIR, req.url === '/' ? 'index.html' : req.url); + + const extname = String(path.extname(filePath)).toLowerCase(); + const mimeTypes = { + '.html': 'text/html', + '.js': 'text/javascript', + '.css': 'text/css', + '.wasm': 'application/wasm' + }; + + const contentType = mimeTypes[extname] || 'application/octet-stream'; + + fs.readFile(filePath, (error, content) => { + if (error) { + if(error.code === 'ENOENT') { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('404 Not Found', 'utf-8'); + } else { + res.writeHead(500); + res.end('Server Error: '+error.code+' ..\n'); + } + } else { + res.writeHead(200, { 'Content-Type': contentType }); + res.end(content, 'utf-8'); + } + }); +}); + +/** + * Runs the E2E tests using Puppeteer. + */ +async function runE2ETests() { + let puppeteer; + try { + puppeteer = require('puppeteer'); + } catch (e) { + console.warn("Puppeteer is not installed. Skipping full browser E2E test."); + console.log("To run full E2E tests, install puppeteer: npm install puppeteer"); + + // At least verify files exist to satisfy the asset check + const assets = ['index.html', 'style.css', 'app.js', 'wasm-worker.js', 'barracuda.wasm', 'barracuda.js']; + for (const asset of assets) { + if (!fs.existsSync(path.join(WEB_DIR, asset))) { + console.error("Asset " + asset + " is missing."); + process.exit(1); + } + } + console.log("All web assets are present."); + process.exit(0); + return; + } + + console.log("Starting Puppeteer E2E tests..."); + const browser = await puppeteer.launch({ headless: 'new' }); + const page = await browser.newPage(); + + const errors = []; + page.on('pageerror', err => errors.push(err.toString())); + page.on('requestfailed', request => { + errors.push("Request failed: " + request.url() + " (" + request.failure().errorText + ")"); + }); + + await page.goto("http://localhost:" + PORT + "/", { waitUntil: 'networkidle0' }); + + // Verify UI Elements exist + const btnText = await page.$eval('#compile-btn', el => el.textContent); + if (!btnText.includes('Compile') && !btnText.includes('Loading')) { + throw new Error("Compile button not found or incorrect text."); + } + + // Wait for worker to be ready + await page.waitForFunction(() => { + const btn = document.getElementById('compile-btn'); + return btn && btn.disabled === false && btn.textContent === 'Compile'; + }, { timeout: 10000 }); + + console.log("Worker initialized in browser."); + + const examples = ['vector_add', 'matmul']; + const targets = ['--nvidia-ptx', '--amdgpu', '--tensix', '--cpu']; + + for (const example of examples) { + for (const target of targets) { + console.log("Testing combination: Example=" + example + ", Target=" + target + " ..."); + + // Select Example + await page.select('#example-select', example); + await new Promise(r => setTimeout(r, 500)); // wait for editor update + + // Select Target + await page.select('#target-select', target); + + // Trigger Compile + await page.click('#compile-btn'); + + // Wait for compilation to finish (button re-enables) + await page.waitForFunction(() => { + const btn = document.getElementById('compile-btn'); + return btn && btn.disabled === false; + }, { timeout: 15000 }); + + const consoleOut = await page.$eval('#console-view', el => el.value); + const outputText = await page.$eval('#output-view', el => el.value); + + if (consoleOut.includes('failed with exit code')) { + console.warn("Warning: Compilation failed for " + example + " with " + target + ". Console:", consoleOut); + } else if (outputText.includes('could not read output file')) { + throw new Error("Output file reading failed for " + example + " with " + target); + } else { + console.log("Combination " + example + " + " + target + " compiled successfully."); + } + } + } + + if (errors.length > 0) { + console.error("Browser errors encountered:", errors); + throw new Error("Browser E2E encountered errors."); + } + + await browser.close(); + console.log("All E2E tests passed successfully!"); + process.exit(0); +} + +// Start Server and Run Tests +server.listen(PORT, () => { + console.log("Test HTTP server running at http://localhost:" + PORT); + runE2ETests().catch(err => { + console.error("E2E Test Failed:", err); + process.exit(1); + }); +}); diff --git a/tests/test_wasm_build.sh b/tests/test_wasm_build.sh new file mode 100755 index 0000000..3fe3106 --- /dev/null +++ b/tests/test_wasm_build.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# test_wasm_build.sh: Test script to verify WASM build succeeds and produces outputs. + +# Fail on any error +set -e + +echo "Running WASM build..." +make wasm + +if [ -f "web/barracuda.js" ] && [ -f "web/barracuda.wasm" ]; then + echo "WASM artifacts generated successfully." + exit 0 +else + echo "Error: WASM artifacts missing." + exit 1 +fi diff --git a/tests/test_wasm_run.js b/tests/test_wasm_run.js new file mode 100644 index 0000000..ef1ae92 --- /dev/null +++ b/tests/test_wasm_run.js @@ -0,0 +1,73 @@ +/** + * test_wasm_run.js + * + * Sanity test to verify the BarraCUDA WebAssembly build works in Node.js. + * This loads the compiled module, waits for the runtime to initialize, + * writes a dummy input file via Emscripten's FS API, calls the compiler main function, + * and reads the output file to ensure everything works correctly. + */ + +const Module = require('../web/barracuda.js'); + +Module.onRuntimeInitialized = () => { + console.log("WASM Runtime initialized. Running tests..."); + + // Test 1: Run with --version + try { + console.log("Testing callMain(['--version'])..."); + // callMain returns the exit code + const versionRet = Module.callMain(['--version']); + if (versionRet !== 0) { + console.error("Error: Expected exit code 0 for --version, got", versionRet); + process.exit(1); + } + console.log("Version check passed."); + } catch (e) { + console.error("Exception during version check:", e); + process.exit(1); + } + + // Test 2: File I/O via Virtual FS + try { + console.log("Testing File I/O..."); + + // Write a dummy CUDA file into the MEMFS + const sourceCode = ` + __global__ void vector_add(float *out, float *a, float *b, int n) { + int tid = blockIdx.x * blockDim.x + threadIdx.x; + if (tid < n) out[tid] = a[tid] + b[tid]; + }`; + + Module.FS.writeFile('/test_input.cu', sourceCode); + + // Compile the dummy file (assuming default flags or minimal flags) + // We output to /test_output.ptx + const compileRet = Module.callMain(['/test_input.cu', '-o', '/test_output.ptx', '--nvidia-ptx']); + + if (compileRet !== 0) { + console.error("Error: Compiler failed on test_input.cu, exit code:", compileRet); + process.exit(1); + } + + // Verify output file exists and has content + const outStat = Module.FS.stat('/test_output.ptx'); + if (outStat.size === 0) { + console.error("Error: Output file is empty."); + process.exit(1); + } + + const outputCode = Module.FS.readFile('/test_output.ptx', { encoding: 'utf8' }); + if (!outputCode.includes('.entry vector_add')) { + console.error("Error: Output PTX does not contain expected 'vector_add' entry. Got:\n", outputCode); + process.exit(1); + } + + console.log("File I/O test passed."); + console.log("All WASM run tests passed successfully!"); + process.exit(0); + + } catch (e) { + console.error("Exception during file I/O test:", e); + process.exit(1); + } +}; diff --git a/tests/test_worker.js b/tests/test_worker.js new file mode 100644 index 0000000..e7ade2a --- /dev/null +++ b/tests/test_worker.js @@ -0,0 +1,74 @@ +/** + * @file test_worker.js + * @description Sanity test for wasm-worker.js using Node.js worker_threads. + */ + +const { Worker } = require('worker_threads'); +const path = require('path'); +const fs = require('fs'); + +const workerPath = path.resolve(__dirname, '../web/wasm-worker.js'); + +console.log("Starting test for wasm-worker.js..."); + +const worker = new Worker(workerPath); + +let isReady = false; + +worker.on('message', (msg) => { + if (msg.type === 'ready') { + console.log("Worker reported ready state."); + isReady = true; + + // Send compile command + const sourceCode = ` + __global__ void test_worker(float *out, int n) { + int tid = threadIdx.x; + if (tid < n) out[tid] = 1.0f; + }`; + + console.log("Sending compile command..."); + worker.postMessage({ + command: 'compile', + source: sourceCode, + args: ['/input.cu', '-o', '/out.ptx', '--nvidia-ptx'] + }); + + } else if (msg.type === 'stdout') { + console.log("[Worker stdout]:", msg.payload); + } else if (msg.type === 'stderr') { + console.log("[Worker stderr]:", msg.payload); + } else if (msg.type === 'compile_result') { + console.log("Worker returned compile result."); + + if (msg.exitCode !== 0) { + console.error("Error: Worker compilation failed with exit code", msg.exitCode); + if (msg.error) console.error("Error Details:", msg.error); + cleanUpAndExit(1); + } + + if (!msg.output || !msg.output.includes('.entry test_worker')) { + console.error("Error: Worker compilation output invalid. Got:\n", msg.output); + cleanUpAndExit(1); + } + + console.log("Worker compilation and output reading passed!"); + cleanUpAndExit(0); + } +}); + +worker.on('error', (err) => { + console.error("Worker encountered an error:", err); + cleanUpAndExit(1); +}); + +worker.on('exit', (code) => { + if (code !== 0) { + console.error("Worker stopped with exit code " + code); + cleanUpAndExit(code); + } +}); + +function cleanUpAndExit(code) { + process.exit(code); +} diff --git a/web/app.js b/web/app.js new file mode 100644 index 0000000..c2b2090 --- /dev/null +++ b/web/app.js @@ -0,0 +1,209 @@ +/** + * @file app.js + * @description Main application logic for the BarraCUDA Web Compiler UI. + * Handles DOM interactions, Monaco editor initialization, and Web Worker communication. + */ + +// Pre-defined code snippets for the examples dropdown +const EXAMPLES = { + 'vector_add': { + name: 'CUDA Vector Add', + language: 'cpp', + code: `__global__ void vector_add(float *out, float *a, float *b, int n) { + int tid = blockIdx.x * blockDim.x + threadIdx.x; + if (tid < n) { + out[tid] = a[tid] + b[tid]; + } +}` + }, + 'matmul': { + name: 'Triton Matmul (Stub)', + language: 'python', + code: `import triton +import triton.language as tl + +@triton.jit +def matmul_kernel( + a_ptr, b_ptr, c_ptr, + M, N, K, + stride_am, stride_ak, + stride_bk, stride_bn, + stride_cm, stride_cn, + BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_N: tl.constexpr, BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(axis=0) + # Simplified matmul stub for testing parser/lexer + pass +` + } +}; + +// DOM Elements +const editorContainer = document.getElementById('editor-container'); +const exampleSelect = document.getElementById('example-select'); +const targetSelect = document.getElementById('target-select'); +const compileBtn = document.getElementById('compile-btn'); +const outputView = document.getElementById('output-view'); +const consoleView = document.getElementById('console-view'); + +// State +let editor = null; +let compilerWorker = null; +let isWorkerReady = false; + +/** + * Initializes the Monaco Editor. + */ +function initEditor() { + require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.41.0/min/vs' }}); + require(['vs/editor/editor.main'], function() { + editor = monaco.editor.create(editorContainer, { + value: EXAMPLES['vector_add'].code, + language: 'cpp', + theme: 'vs-dark', + automaticLayout: true, + minimap: { enabled: false } + }); + }); +} + +/** + * Populates the Examples dropdown menu. + */ +function initDropdown() { + for (const [key, ex] of Object.entries(EXAMPLES)) { + const option = document.createElement('option'); + option.value = key; + option.textContent = ex.name; + exampleSelect.appendChild(option); + } + + exampleSelect.addEventListener('change', (e) => { + const selected = EXAMPLES[e.target.value]; + if (editor) { + editor.setValue(selected.code); + monaco.editor.setModelLanguage(editor.getModel(), selected.language); + } + }); +} + +/** + * Initializes the Web Worker for background compilation. + */ +function initWorker() { + // Determine worker script path + compilerWorker = new Worker('wasm-worker.js'); + + compilerWorker.onmessage = function(e) { + const msg = e.data; + switch (msg.type) { + case 'ready': + isWorkerReady = true; + compileBtn.disabled = false; + compileBtn.textContent = 'Compile'; + appendConsoleLog("Compiler runtime ready.\n"); + break; + case 'stdout': + appendConsoleLog(msg.payload + "\n"); + break; + case 'stderr': + appendConsoleLog("ERROR: " + msg.payload + "\n"); + break; + case 'compile_result': + handleCompileResult(msg); + break; + } + }; + + compilerWorker.onerror = function(err) { + appendConsoleLog("Worker error: " + err.message + "\n"); + resetCompileButton(); + }; +} + +/** + * Appends text to the console view and scrolls to the bottom. + * @param {string} text - The text to append. + */ +function appendConsoleLog(text) { + consoleView.value += text; + consoleView.scrollTop = consoleView.scrollHeight; +} + +/** + * Handles the completion of the compilation process. + * @param {Object} msg - The result message from the worker. + */ +function handleCompileResult(msg) { + resetCompileButton(); + + if (msg.exitCode === 0) { + appendConsoleLog("\\nCompilation succeeded.\\n"); + outputView.value = msg.output || "// No output generated."; + } else { + appendConsoleLog("\\nCompilation failed with exit code " + msg.exitCode + ".\\n"); + outputView.value = msg.error ? msg.error : "// Compilation failed. See console for details."; + } +} + +/** + * Resets the compile button state. + */ +function resetCompileButton() { + compileBtn.disabled = false; + compileBtn.textContent = 'Compile'; +} + +/** + * Triggers the compilation process by sending a message to the Web Worker. + */ +function doCompile() { + if (!isWorkerReady || !compilerWorker || !editor) return; + + // Clear previous output + outputView.value = ''; + appendConsoleLog("\n--- Starting Compilation ---\n"); + + compileBtn.disabled = true; + compileBtn.textContent = 'Compiling...'; + + const sourceCode = editor.getValue(); + const target = targetSelect.value; + + // Determine input file extension based on language + const currentLang = exampleSelect.value === 'matmul' ? '.py' : '.cu'; + const inputFilename = '/input' + currentLang; + + // Determine output file extension + let outExt = '.ptx'; + if (target === '--amdgpu') outExt = '.s'; + if (target === '--cpu') outExt = '.o'; + if (target === '--tensix') outExt = '.elf'; // just a guess + + const outputFilename = '/out' + outExt; + + const args = [inputFilename, '-o', outputFilename, target]; + + // Add --triton flag if it's a python file + if (currentLang === '.py') { + args.push('--triton'); + } + + compilerWorker.postMessage({ + command: 'compile', + source: sourceCode, + args: args + }); +} + +// Bootstrap +window.addEventListener('DOMContentLoaded', () => { + compileBtn.disabled = true; + compileBtn.textContent = 'Loading...'; + + initDropdown(); + initEditor(); + initWorker(); + + compileBtn.addEventListener('click', doCompile); +}); diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..a67a713 --- /dev/null +++ b/web/index.html @@ -0,0 +1,66 @@ + + + + + + + BarraCUDA Web Compiler + + + + + +
+

BarraCUDA Web Compiler

+ +
+ + + + + + + + +
+
+ + +
+ +
+
Source Code
+ +
+
+ + +
+ +
+
Compiled Output
+ + +
+ +
+
Console
+ + +
+
+
+ + + + + + + diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..df52766 --- /dev/null +++ b/web/style.css @@ -0,0 +1,187 @@ +/* + * style.css + * Vanilla CSS styling for the BarraCUDA Web Compiler. + * Focuses on a modern, responsive layout without relying on external frameworks. + */ + +/* Basic resets and base styles for the document */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + background-color: #1e1e1e; + color: #cccccc; + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; /* Prevent body scroll, handle scrolling within panes */ +} + +/* Header layout using flexbox to space out title and controls */ +.app-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 20px; + background-color: #252526; + border-bottom: 1px solid #333333; +} + +.app-header h1 { + font-size: 1.2rem; + color: #ffffff; +} + +/* Controls grouping (dropdowns and button) */ +.controls { + display: flex; + gap: 10px; +} + +select, button { + padding: 6px 12px; + font-size: 0.9rem; + border-radius: 4px; + border: 1px solid #3c3c3c; + background-color: #333333; + color: #ffffff; + outline: none; +} + +button { + background-color: #0e639c; + border: 1px solid #0e639c; + cursor: pointer; + font-weight: bold; + transition: background-color 0.2s ease; +} + +button:hover { + background-color: #1177bb; +} + +button:disabled { + background-color: #4d4d4d; + border-color: #4d4d4d; + color: #888888; + cursor: not-allowed; +} + +/* Main application area using CSS Grid for a 50/50 split on desktop */ +.app-main { + display: grid; + grid-template-columns: 1fr 1fr; + flex: 1; + overflow: hidden; +} + +/* Base style for panes */ +.pane { + display: flex; + flex-direction: column; + border-right: 1px solid #333333; + background-color: #1e1e1e; +} + +.pane:last-child { + border-right: none; +} + +.pane header { + padding: 5px 10px; + background-color: #2d2d2d; + font-size: 0.8rem; + font-weight: bold; + text-transform: uppercase; + color: #aaaaaa; + border-bottom: 1px solid #333333; +} + +/* Editor container needs to fill the remaining space */ +#editor-container { + flex: 1; + position: relative; +} + +/* Right pane is further split vertically into Output and Console */ +.right-pane { + display: grid; + grid-template-rows: 60% 40%; +} + +.output-pane, .console-pane { + display: flex; + flex-direction: column; + border-bottom: 1px solid #333333; +} + +.console-pane { + border-bottom: none; +} + +/* Textareas for displaying output and logs */ +textarea { + flex: 1; + width: 100%; + resize: none; + background-color: #1e1e1e; + color: #d4d4d4; + border: none; + padding: 10px; + font-family: Consolas, "Courier New", monospace; + font-size: 0.85rem; + outline: none; + line-height: 1.4; +} + +/* Custom scrollbars for a polished look */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} +::-webkit-scrollbar-track { + background: #1e1e1e; +} +::-webkit-scrollbar-thumb { + background: #424242; +} +::-webkit-scrollbar-thumb:hover { + background: #4f4f4f; +} + +/* Responsive Media Query: Stack panes vertically on mobile screens (max-width: 768px) */ +@media (max-width: 768px) { + .app-header { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .controls { + width: 100%; + flex-wrap: wrap; + } + + .controls select, .controls button { + flex: 1; + } + + .app-main { + grid-template-columns: 1fr; + grid-template-rows: 50vh auto auto; + overflow-y: auto; + } + + .pane { + border-right: none; + border-bottom: 1px solid #333333; + } + + .right-pane { + grid-template-rows: 40vh 30vh; + } +} diff --git a/web/wasm-worker.js b/web/wasm-worker.js new file mode 100644 index 0000000..20c9610 --- /dev/null +++ b/web/wasm-worker.js @@ -0,0 +1,149 @@ +/** + * @file wasm-worker.js + * @description Web Worker script to handle asynchronous BarraCUDA compilation. + * This runs the WebAssembly compiler off the main thread to keep the UI responsive. + */ + +/** + * Emscripten module configuration object. + * We override print and printErr to capture standard output and standard error + * and post them back to the main thread. + * @type {Object} + */ +var Module = { + /** + * Captures standard output from the WASM module. + * @param {string} text - The output string. + */ + print: function(text) { + postMessage({ type: 'stdout', payload: text }); + }, + + /** + * Captures standard error from the WASM module. + * @param {string} text - The error string. + */ + printErr: function(text) { + postMessage({ type: 'stderr', payload: text }); + }, + + /** + * Called when the WASM runtime has fully initialized. + */ + onRuntimeInitialized: function() { + postMessage({ type: 'ready' }); + } +}; + +// In a browser environment, importScripts is available. +// In Node.js testing environment, it might not be, so we handle it gracefully. +if (typeof importScripts === 'function') { + importScripts('barracuda.js'); +} else if (typeof require === 'function') { + // For Node.js Worker Thread testing + const path = require('path'); + const { parentPort } = require('worker_threads'); + + // Mock self and postMessage for Node.js worker_threads + global.self = {}; + global.postMessage = (msg) => { + if (parentPort) parentPort.postMessage(msg); + }; + + // Inject our Module into global so Emscripten might pick it up, + // but also we assign it explicitly. Actually, the easiest way to override + // Emscripten in CommonJS is to just set properties on the exported module. + const wasmPath = path.resolve(__dirname, '../web/barracuda.js'); + const emscriptenModule = require(wasmPath); + + // Patch the emscripten module with our handlers + emscriptenModule.print = Module.print; + emscriptenModule.printErr = Module.printErr; + emscriptenModule.onRuntimeInitialized = Module.onRuntimeInitialized; + + // Replace our local Module variable with the fully loaded one + Module = emscriptenModule; + + // In Node.js, we must manually trigger onRuntimeInitialized if it already initialized + if (Module.calledRun) { + Module.onRuntimeInitialized(); + } + + // Hook parentPort to self.onmessage + if (parentPort) { + parentPort.on('message', (msg) => { + if (typeof self.onmessage === 'function') { + self.onmessage({ data: msg }); + } + }); + } +} + +/** + * Listens for messages from the main thread. + * Expected message format: + * { + * command: 'compile', + * source: string, + * args: Array + * } + */ +self.onmessage = function(e) { + const data = e.data; + + if (data.command === 'compile') { + const sourceCode = data.source || ''; + // Extract output filename from args to read it back later. + // We look for "-o" and take the next argument. + let outputFilename = '/out.ptx'; // default fallback + const args = data.args || []; + + for (let i = 0; i < args.length - 1; i++) { + if (args[i] === '-o') { + outputFilename = args[i+1]; + break; + } + } + + try { + // Write the incoming source code to a virtual file + Module.FS.writeFile(args[0] || '/input.cu', sourceCode); + + // Execute the compiler + // Expected args: e.g. ['/input.cu', '-o', '/out.ptx', '--nvidia-ptx'] + const exitCode = Module.callMain(args); + + let outputData = null; + + if (exitCode === 0) { + // Read the generated output file from the virtual file system + try { + const stat = Module.FS.stat(outputFilename); + if (stat && stat.size > 0) { + outputData = Module.FS.readFile(outputFilename, { encoding: 'utf8' }); + } else { + outputData = "// Compilation succeeded, but output file is empty."; + } + } catch (err) { + outputData = "// Compilation succeeded, but could not read output file."; + } + } + + // Send the result back to the main thread + postMessage({ + type: 'compile_result', + exitCode: exitCode, + output: outputData + }); + + } catch (err) { + // Catch any unexpected errors from the Emscripten runtime or FS + postMessage({ + type: 'compile_result', + exitCode: -1, + output: null, + error: err.message || err.toString() + }); + } + } +};