diff --git a/.gitignore b/.gitignore index a6e0752889..3a06a49298 100644 --- a/.gitignore +++ b/.gitignore @@ -139,6 +139,8 @@ generated/ /java/gradle/build /core/examples/build /java/gradle/example/.processing + +libprocessing/ffi/include/* /app/windows/obj /java/android/example/build /java/android/example/.processing diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..f430a4bd23 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "libprocessing"] + path = libprocessing + url = https://github.com/processing/libprocessing diff --git a/BUILD.md b/BUILD.md index 1216f2e952..7b1fc792ce 100644 --- a/BUILD.md +++ b/BUILD.md @@ -42,22 +42,62 @@ If you don't have them installed, you will need to install [Git](https://git-scm 1. **Clone the repository:** ```bash - git clone https://github.com/processing/processing4.git + git clone --recursive https://github.com/processing/processing4.git cd processing4 ``` 2. **Install Temurin JDK 17:** - - Download and install the appropriate version for your platform: + +Processing requires the Temurin distribution of OpenJDK. - - [Linux (x86)](https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.15%2B6/OpenJDK17U-jdk_x64_linux_hotspot_17.0.15_6.tar.gz) - - [macOS (Apple Silicon)](https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.15%2B6/OpenJDK17U-jdk_aarch64_mac_hotspot_17.0.15_6.pkg) - - [Other platforms](https://adoptium.net/temurin/releases/?package=jdk&version=17&os=any&arch=any) +You can download it from [Adoptium](https://adoptium.net/), from [GitHub releases](https://github.com/adoptium/temurin17-binaries/releases), +or find it in the package manager for your platform. + +### macOS: +```bash +brew install --cask temurin@17 +```` + +### Windows (using winget): +```bash +winget install --id=EclipseAdoptium.Temurin.17.JDK -e +``` + +### SDKMAN! + +[SDKMAN!](https://sdkman.io/) is a useful tool for developers working on multiple versions of the JVM. + +## WebGPU Support (Optional) + +To build Processing with the experimental WebGPU renderer, you need JDK 25, Rust, and jextract. + +### Install Temurin JDK 25 + +```bash +brew install --cask temurin@25 # macOS +``` + +### Install `jextract` + +`jextract` generates Java bindings from C header files. +You can download it [here](https://jdk.java.net/jextract/) or install it using SDKMAN!: + +```bash +sdk install jextract +```` + +### Build with WebGPU + +```bash +./gradlew build -PenableWebGPU=true +``` 3. **Set the `JAVA_HOME` environment variable:** +It may be necessary to set the `JAVA_HOME` environment variable to point to your Temurin JDK installation. + ```bash - export JAVA_HOME=/path/to/temurin/jdk-17.0.15+6/ + export JAVA_HOME=/path/to/temurin/jdk-17/ ``` ### Build, Run, and Package Processing @@ -138,10 +178,10 @@ If you’re building Processing using IntelliJ IDEA and something’s not workin ### Use the Correct JDK (temurin-17) -Make sure IntelliJ is using **temurin-17**, not another version. Some users have reported issues with ms-17. +Make sure IntelliJ is using **temurin-17**, not another version. If building with WebGPU (`-PenableWebGPU=true`), use **temurin-25** instead. 1. Go to **File > Project Structure > Project** -2. Set the **Project SDK** to: `temurin-17 java version "17.0.15"` +2. Set the **Project SDK** to: `temurin-17` ![JDK Selection](.github/media/troubleshooting-Intellij-setting-djk-version-manually.png) @@ -155,7 +195,6 @@ If it is not already installed, you can download it by: Now go back to your main window and 1. Click the green Run Icon in the top right of the window. - ### “Duplicate content roots detected” You may see this warning in IntelliJ: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b0fabe63e6..f19051afa3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -51,14 +51,17 @@ compose.desktop { application { mainClass = "processing.app.ProcessingKt" - jvmArgs(*listOf( - Pair("processing.version", rootProject.version), - Pair("processing.revision", findProperty("revision") ?: Int.MAX_VALUE), - Pair("processing.contributions.source", "https://contributions.processing.org/contribs"), - Pair("processing.download.page", "https://processing.org/download/"), - Pair("processing.download.latest", "https://processing.org/download/latest.txt"), - Pair("processing.tutorials", "https://processing.org/tutorials/"), - ).map { "-D${it.first}=${it.second}" }.toTypedArray()) + jvmArgs( + "--enable-native-access=ALL-UNNAMED", // Required for Java 25 native library access + *listOf( + Pair("processing.version", rootProject.version), + Pair("processing.revision", findProperty("revision") ?: Int.MAX_VALUE), + Pair("processing.contributions.source", "https://contributions.processing.org/contribs"), + Pair("processing.download.page", "https://processing.org/download/"), + Pair("processing.download.latest", "https://processing.org/download/latest.txt"), + Pair("processing.tutorials", "https://processing.org/tutorials/"), + ).map { "-D${it.first}=${it.second}" }.toTypedArray() + ) nativeDistributions{ modules("jdk.jdi", "java.compiler", "jdk.accessibility", "jdk.zipfs", "java.management.rmi", "java.scripting", "jdk.httpserver") @@ -432,8 +435,15 @@ tasks.register("includeJavaMode") { duplicatesStrategy = DuplicatesStrategy.EXCLUDE dirPermissions { unix("rwx------") } } +val enableWebGPU = findProperty("enableWebGPU")?.toString()?.toBoolean() ?: false + tasks.register("includeJdk") { - from(Jvm.current().javaHome.absolutePath) + val jdkVersion = if (enableWebGPU) 25 else 17 + val jdkHome = project.the().launcherFor { + languageVersion.set(JavaLanguageVersion.of(jdkVersion)) + }.map { it.metadata.installationPath.asFile } + + from(jdkHome) destinationDir = composeResources("jdk").get().asFile fileTree(destinationDir).files.forEach { file -> @@ -516,7 +526,7 @@ tasks.register("includeProcessingResources"){ finalizedBy("signResources") } -tasks.register("signResources"){ +tasks.register("signResources") { onlyIf { OperatingSystem.current().isMacOsX && @@ -561,10 +571,11 @@ tasks.register("signResources"){ exclude("*.jar") exclude("*.so") exclude("*.dll") - }.forEach{ file -> - exec { - commandLine("codesign", "--timestamp", "--force", "--deep","--options=runtime", "--sign", "Developer ID Application", file) - } + }.forEach{ f -> + ProcessBuilder("codesign", "--timestamp", "--force", "--deep", "--options=runtime", "--sign", "Developer ID Application", f.absolutePath) + .inheritIO() + .start() + .waitFor() } jars.forEach { file -> FileOutputStream(File(file.parentFile, file.nameWithoutExtension)).use { fos -> diff --git a/build.gradle.kts b/build.gradle.kts index 371e34bc29..ca163e476c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,35 @@ plugins { // Can be deleted after the migration to Gradle is complete layout.buildDirectory = file(".build") +val enableWebGPU = findProperty("enableWebGPU")?.toString()?.toBoolean() ?: false +val javaVersion = if (enableWebGPU) "25" else "17" +val kotlinJvmTarget = if (enableWebGPU) { + org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_25 +} else { + org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 +} + +allprojects { + tasks.withType().configureEach { + sourceCompatibility = javaVersion + targetCompatibility = javaVersion + } + + tasks.withType().configureEach { + compilerOptions { + jvmTarget.set(kotlinJvmTarget) + } + } + + plugins.withType { + extensions.configure { + toolchain { + languageVersion.set(JavaLanguageVersion.of(javaVersion.toInt())) + } + } + } +} + // Configure the dependencyUpdates task tasks { dependencyUpdates { diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000000..876c922b22 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() +} diff --git a/buildSrc/src/main/kotlin/processing/gradle/CargoBuildTask.kt b/buildSrc/src/main/kotlin/processing/gradle/CargoBuildTask.kt new file mode 100644 index 0000000000..0b73d75ac6 --- /dev/null +++ b/buildSrc/src/main/kotlin/processing/gradle/CargoBuildTask.kt @@ -0,0 +1,56 @@ +package processing.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.gradle.process.ExecOperations +import javax.inject.Inject + +abstract class CargoBuildTask : DefaultTask() { + + @get:Inject + abstract val execOperations: ExecOperations + + @get:InputDirectory + abstract val cargoWorkspaceDir: DirectoryProperty + + @get:Input + abstract val manifestPath: Property + + @get:Input + abstract val release: Property + + @get:Input + abstract val cargoPath: Property + + @get:OutputFile + abstract val outputLibrary: RegularFileProperty + + init { + group = "rust" + description = "Builds Rust library using cargo" + + // release by default + release.convention(true) + } + + @TaskAction + fun build() { + val buildType = if (release.get()) "release" else "debug" + logger.lifecycle("Building Rust library ($buildType mode)...") + + val args = mutableListOf("build") + if (release.get()) { + args.add("--release") + } + args.add("--manifest-path") + args.add(manifestPath.get()) + + execOperations.exec { + workingDir = cargoWorkspaceDir.get().asFile + commandLine = listOf(cargoPath.get()) + args + } + } +} diff --git a/buildSrc/src/main/kotlin/processing/gradle/CargoCleanTask.kt b/buildSrc/src/main/kotlin/processing/gradle/CargoCleanTask.kt new file mode 100644 index 0000000000..1caf9133bf --- /dev/null +++ b/buildSrc/src/main/kotlin/processing/gradle/CargoCleanTask.kt @@ -0,0 +1,38 @@ +package processing.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.gradle.process.ExecOperations +import javax.inject.Inject + +abstract class CargoCleanTask : DefaultTask() { + + @get:Inject + abstract val execOperations: ExecOperations + + @get:InputDirectory + abstract val cargoWorkspaceDir: DirectoryProperty + + @get:Input + abstract val manifestPath: Property + + @get:Input + abstract val cargoPath: Property + + init { + group = "rust" + description = "Cleans Rust build artifacts" + } + + @TaskAction + fun clean() { + logger.lifecycle("Cleaning Rust build artifacts...") + + execOperations.exec { + workingDir = cargoWorkspaceDir.get().asFile + commandLine(cargoPath.get(), "clean", "--manifest-path", manifestPath.get()) + } + } +} diff --git a/buildSrc/src/main/kotlin/processing/gradle/DownloadJextractTask.kt b/buildSrc/src/main/kotlin/processing/gradle/DownloadJextractTask.kt new file mode 100644 index 0000000000..254195a38d --- /dev/null +++ b/buildSrc/src/main/kotlin/processing/gradle/DownloadJextractTask.kt @@ -0,0 +1,60 @@ +package processing.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import java.net.URI + +abstract class DownloadJextractTask : DefaultTask() { + + @get:Input + abstract val jextractVersion: Property + + @get:Input + abstract val platform: Property + + @get:OutputDirectory + abstract val jextractDir: DirectoryProperty + + @get:Internal + abstract val downloadTarball: RegularFileProperty + + init { + group = "rust" + description = "Downloads and extracts jextract for the current platform" + } + + @TaskAction + fun download() { + val version = jextractVersion.get() + val plat = platform.get() + val fileName = "openjdk-$version" + "_${plat}_bin.tar.gz" + val downloadUrl = "https://download.java.net/java/early_access/jextract/22/6/$fileName" + val tarFile = downloadTarball.get().asFile + + if (!tarFile.exists()) { + logger.lifecycle("Downloading jextract from $downloadUrl") + try { + tarFile.outputStream().use { output -> + URI.create(downloadUrl).toURL().openStream().use { input -> + input.copyTo(output) + } + } + } catch (e: Exception) { + throw GradleException("Failed to download jextract: ${e.message}", e) + } + } + + val extractDir = jextractDir.get().asFile + logger.lifecycle("Extracting jextract to ${extractDir.parent}") + project.copy { + from(project.tarTree(tarFile)) + into(extractDir.parent) + } + + logger.lifecycle("jextract extracted to: $extractDir") + } +} diff --git a/buildSrc/src/main/kotlin/processing/gradle/GenerateJextractBindingsTask.kt b/buildSrc/src/main/kotlin/processing/gradle/GenerateJextractBindingsTask.kt new file mode 100644 index 0000000000..22fc9c240c --- /dev/null +++ b/buildSrc/src/main/kotlin/processing/gradle/GenerateJextractBindingsTask.kt @@ -0,0 +1,49 @@ +package processing.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.gradle.process.ExecOperations +import javax.inject.Inject + +abstract class GenerateJextractBindingsTask : DefaultTask() { + + @get:Inject + abstract val execOperations: ExecOperations + + @get:InputFile + abstract val headerFile: RegularFileProperty + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + @get:Input + abstract val targetPackage: Property + + @get:Input + abstract val jextractPath: Property + + init { + group = "rust" + description = "Generates Java Panama FFM bindings from C headers" + } + + @TaskAction + fun generate() { + val outDir = outputDirectory.get().asFile + outDir.mkdirs() + + logger.lifecycle("Generating Java bindings from ${headerFile.get().asFile}...") + + execOperations.exec { + commandLine( + jextractPath.get(), + "--output", outDir.absolutePath, + "--target-package", targetPackage.get(), + headerFile.get().asFile.absolutePath + ) + } + } +} diff --git a/buildSrc/src/main/kotlin/processing/gradle/JextractUtils.kt b/buildSrc/src/main/kotlin/processing/gradle/JextractUtils.kt new file mode 100644 index 0000000000..505a222a59 --- /dev/null +++ b/buildSrc/src/main/kotlin/processing/gradle/JextractUtils.kt @@ -0,0 +1,29 @@ +package processing.gradle + +object JextractUtils { + fun findUserJextract(): String? { + val jextractHome = System.getenv("JEXTRACT_HOME") ?: return null + + val isWindows = System.getProperty("os.name").lowercase().contains("windows") + val path = if (isWindows) { + "$jextractHome/bin/jextract.bat" + } else { + "$jextractHome/bin/jextract" + } + + val file = java.io.File(path) + if (file.exists()) { + return path + } + + return null + } + + fun getExecutableName(): String { + return if (System.getProperty("os.name").lowercase().contains("windows")) { + "jextract.bat" + } else { + "jextract" + } + } +} diff --git a/buildSrc/src/main/kotlin/processing/gradle/PlatformUtils.kt b/buildSrc/src/main/kotlin/processing/gradle/PlatformUtils.kt new file mode 100644 index 0000000000..f442dba985 --- /dev/null +++ b/buildSrc/src/main/kotlin/processing/gradle/PlatformUtils.kt @@ -0,0 +1,53 @@ +package processing.gradle + +import org.gradle.api.GradleException + +object PlatformUtils { + data class Platform( + val os: String, + val arch: String, + val libExtension: String, + val target: String + ) { + val libName: String + get() = if (os == "windows") "processing.$libExtension" else "libprocessing.$libExtension" + + val jextractPlatform: String + get() { + val jextractArch = if (arch == "x86_64") "x64" else arch + return "$os-$jextractArch" + } + } + + fun detect(): Platform { + val osName = System.getProperty("os.name").lowercase() + val osArch = System.getProperty("os.arch").lowercase() + + val os = when { + osName.contains("mac") || osName.contains("darwin") -> "macos" + osName.contains("win") -> "windows" + osName.contains("linux") -> "linux" + else -> throw GradleException("Unsupported OS: $osName") + } + + val arch = when { + osArch.contains("aarch64") || osArch.contains("arm") -> "aarch64" + osArch.contains("x86_64") || osArch.contains("amd64") -> "x86_64" + else -> throw GradleException("Unsupported architecture: $osArch") + } + + val libExtension = when (os) { + "macos" -> "dylib" + "windows" -> "dll" + "linux" -> "so" + else -> throw GradleException("Unknown platform: $os") + } + + return Platform(os, arch, libExtension, "$os-$arch") + } + + fun getCargoPath(): String { + return System.getenv("CARGO_HOME")?.let { "$it/bin/cargo" } + ?: "${System.getProperty("user.home")}/.cargo/bin/cargo" + } +} diff --git a/buildSrc/src/main/kotlin/processing/gradle/SignResourcesTask.kt b/buildSrc/src/main/kotlin/processing/gradle/SignResourcesTask.kt new file mode 100644 index 0000000000..91be783ea3 --- /dev/null +++ b/buildSrc/src/main/kotlin/processing/gradle/SignResourcesTask.kt @@ -0,0 +1,108 @@ +package processing.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.TaskAction +import org.gradle.process.ExecOperations +import java.io.File +import java.io.FileOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import javax.inject.Inject + +abstract class SignResourcesTask : DefaultTask() { + + @get:Inject + abstract val execOperations: ExecOperations + + @get:InputDirectory + abstract val resourcesPath: DirectoryProperty + + init { + group = "compose desktop" + description = "Signs macOS resources (binaries and libraries) for distribution" + } + + @TaskAction + fun signResources() { + val resourcesDir = resourcesPath.get().asFile + val jars = mutableListOf() + + // Copy Info.plist if present + project.fileTree(resourcesDir) + .matching { include("**/Info.plist") } + .singleOrNull() + ?.let { file -> + project.copy { + from(file) + into(resourcesDir) + } + } + + // Extract JARs to temporary directories for signing + project.fileTree(resourcesDir) { + include("**/*.jar") + exclude("**/*.jar.tmp/**") + }.forEach { file -> + val tempDir = file.parentFile.resolve("${file.name}.tmp") + project.copy { + from(project.zipTree(file)) + into(tempDir) + } + file.delete() + jars.add(tempDir) + } + + // Sign all binaries and native libraries + project.fileTree(resourcesDir) { + include("**/bin/**") + include("**/*.jnilib") + include("**/*.dylib") + include("**/*aarch64*") + include("**/*x86_64*") + include("**/*ffmpeg*") + include("**/ffmpeg*/**") + exclude("jdk/**") + exclude("*.jar") + exclude("*.so") + exclude("*.dll") + }.forEach { file -> + execOperations.exec { + commandLine( + "codesign", + "--timestamp", + "--force", + "--deep", + "--options=runtime", + "--sign", + "Developer ID Application", + file + ) + } + } + + // Repackage JARs after signing + jars.forEach { file -> + FileOutputStream(File(file.parentFile, file.nameWithoutExtension)).use { fos -> + ZipOutputStream(fos).use { zos -> + file.walkTopDown().forEach { fileEntry -> + if (fileEntry.isFile) { + val zipEntryPath = fileEntry.relativeTo(file).path + val entry = ZipEntry(zipEntryPath) + zos.putNextEntry(entry) + fileEntry.inputStream().use { input -> + input.copyTo(zos) + } + zos.closeEntry() + } + } + } + } + file.deleteRecursively() + } + + // Clean up Info.plist + File(resourcesDir, "Info.plist").delete() + } +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 16593450ec..4a97dde41e 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,4 +1,5 @@ import com.vanniktech.maven.publish.SonatypeHost +import processing.gradle.* plugins { id("java") @@ -11,11 +12,17 @@ repositories { maven { url = uri("https://jogamp.org/deployment/maven") } } +val enableWebGPU = findProperty("enableWebGPU")?.toString()?.toBoolean() ?: false + sourceSets{ main{ java{ srcDirs("src") exclude("**/*.jnilib") + if (!enableWebGPU) { + exclude("processing/webgpu/**") + exclude("processing/ffi/**") + } } resources{ srcDirs("src") @@ -33,9 +40,169 @@ dependencies { implementation(libs.jogl) implementation(libs.gluegen) + if (enableWebGPU) { + val lwjglVersion = "3.3.6" + val lwjglNatives = when { + System.getProperty("os.name").lowercase().contains("mac") -> { + if (System.getProperty("os.arch").contains("aarch64")) { + "natives-macos-arm64" + } else { + "natives-macos" + } + } + System.getProperty("os.name").lowercase().contains("win") -> "natives-windows" + System.getProperty("os.name").lowercase().contains("linux") -> "natives-linux" + else -> "natives-linux" + } + + implementation(platform("org.lwjgl:lwjgl-bom:$lwjglVersion")) + implementation("org.lwjgl", "lwjgl") + implementation("org.lwjgl", "lwjgl-glfw") + runtimeOnly("org.lwjgl", "lwjgl", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-glfw", classifier = lwjglNatives) + } + testImplementation(libs.junit) } +if (enableWebGPU) { + val currentPlatform = PlatformUtils.detect() + val libprocessingDir = file("${project.rootDir}/libprocessing") + + if (!libprocessingDir.exists()) { + throw GradleException( + "libprocessing submodule directory not found at: ${libprocessingDir.absolutePath}\n" + + "Please initialize the submodule with: git submodule update --init --recursive" + ) + } + + val rustTargetDir = file("$libprocessingDir/target") + val nativeOutputDir = file("${layout.buildDirectory.get()}/native/${currentPlatform.target}") + + val ffiManifestPath = fileTree(libprocessingDir) { + include("**/processing_ffi/Cargo.toml") + }.files.firstOrNull()?.let { it.relativeTo(libprocessingDir).path } + ?: throw GradleException( + "Could not find processing_ffi Cargo.toml in libprocessing.\n" + + "Searched in: ${libprocessingDir.absolutePath}\n" + + "The libprocessing structure may have changed." + ) + + val buildRustRelease by tasks.registering(CargoBuildTask::class) { + cargoWorkspaceDir.set(libprocessingDir) + manifestPath.set(ffiManifestPath) + release.set(true) + cargoPath.set(PlatformUtils.getCargoPath()) + outputLibrary.set(file("$rustTargetDir/release/${currentPlatform.libName}")) + + inputs.files(fileTree("$libprocessingDir/crates") { + include("**/src/**/*.rs") + include("**/Cargo.toml") + include("**/build.rs") + include("**/cbindgen.toml") + }) + inputs.file("$libprocessingDir/Cargo.toml") + inputs.file("$libprocessingDir/Cargo.lock") + + val headerDir = file("$libprocessingDir/${ffiManifestPath}").parentFile.resolve("include") + outputs.file("$headerDir/processing.h") + } + + val copyNativeLibs by tasks.registering(Copy::class) { + group = "rust" + description = "Copy processing library to build directory" + + dependsOn(buildRustRelease) + + from("$rustTargetDir/release") { + include(currentPlatform.libName) + } + + into(nativeOutputDir) + } + + val bundleNativeLibs by tasks.registering(Copy::class) { + group = "rust" + description = "Bundle native library into resources" + + dependsOn(copyNativeLibs) + + from(nativeOutputDir) + into("${sourceSets.main.get().output.resourcesDir}/native/${currentPlatform.target}") + } + + val cleanRust by tasks.registering(CargoCleanTask::class) { + cargoWorkspaceDir.set(libprocessingDir) + manifestPath.set(ffiManifestPath) + cargoPath.set(PlatformUtils.getCargoPath()) + + mustRunAfter(buildRustRelease) + } + + tasks.named("clean") { + dependsOn(cleanRust) + } + + val generatedJavaDir = file("${layout.buildDirectory.get()}/generated/sources/jextract/java") + + sourceSets.main { + java.srcDirs(generatedJavaDir) + } + + val jextractVersionString = "22-jextract+6-47" + val jextractDirectory = file("${gradle.gradleUserHomeDir}/jextract-22") + val jextractTarballFile = file("${gradle.gradleUserHomeDir}/jextract-$jextractVersionString.tar.gz") + + val downloadJextract by tasks.registering(DownloadJextractTask::class) { + jextractVersion.set(jextractVersionString) + platform.set(currentPlatform.jextractPlatform) + jextractDir.set(jextractDirectory) + downloadTarball.set(jextractTarballFile) + + onlyIf { !jextractDirectory.exists() } + } + + val makeJextractExecutable by tasks.registering(Exec::class) { + group = "rust" + description = "Make jextract binary executable on Unix systems" + + dependsOn(downloadJextract) + onlyIf { !System.getProperty("os.name").lowercase().contains("windows") } + + val jextractBin = file("$jextractDirectory/bin/jextract") + commandLine("chmod", "+x", jextractBin.absolutePath) + } + + val generateJavaBindings by tasks.registering(GenerateJextractBindingsTask::class) { + dependsOn(buildRustRelease) + + val userJextract = JextractUtils.findUserJextract() + if (userJextract == null) { + dependsOn(downloadJextract, makeJextractExecutable) + } + + // Find header file dynamically based on FFI manifest location + val headerDir = file("$libprocessingDir/${ffiManifestPath}").parentFile.resolve("include") + headerFile.set(file("$headerDir/processing.h")) + outputDirectory.set(generatedJavaDir) + targetPackage.set("processing.ffi") + + jextractPath.set(userJextract ?: "$jextractDirectory/bin/${JextractUtils.getExecutableName()}") + } + + tasks.named("compileJava") { + dependsOn(generateJavaBindings) + } + + tasks.named("compileKotlin") { + dependsOn(generateJavaBindings) + } + + tasks.named("processResources") { + dependsOn(bundleNativeLibs) + } +} + mavenPublishing{ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = true) signAllPublications() diff --git a/core/examples/src/main/java/webgpu/AnimatedMesh.java b/core/examples/src/main/java/webgpu/AnimatedMesh.java new file mode 100644 index 0000000000..c1dec3f049 --- /dev/null +++ b/core/examples/src/main/java/webgpu/AnimatedMesh.java @@ -0,0 +1,63 @@ +package webgpu; + +import processing.core.PApplet; + +public class AnimatedMesh extends PApplet { + + int gridSize = 20; + float spacing = 10; + float time = 0; + + public void settings() { + size(600, 600, WEBGPU); + } + + public void setup() { + perspective(PI/3, (float)width/height, 0.1f, 1000); + camera(150, 150, 150, 0, 0, 0, 0, 1, 0); + } + + public void draw() { + background(13, 13, 26); + + float offset = (gridSize * spacing) / 2.0f; + + beginShape(TRIANGLES); + for (int z = 0; z < gridSize - 1; z++) { + for (int x = 0; x < gridSize - 1; x++) { + float px0 = x * spacing - offset; + float pz0 = z * spacing - offset; + float px1 = (x + 1) * spacing - offset; + float pz1 = (z + 1) * spacing - offset; + + float y00 = wave(px0, pz0); + float y10 = wave(px1, pz0); + float y01 = wave(px0, pz1); + float y11 = wave(px1, pz1); + + fill(x * 255.0f / gridSize, 128, z * 255.0f / gridSize); + normal(0, 1, 0); + + vertex(px0, y00, pz0); + vertex(px0, y01, pz1); + vertex(px1, y10, pz0); + + vertex(px1, y10, pz0); + vertex(px0, y01, pz1); + vertex(px1, y11, pz1); + } + } + endShape(); + + time += 0.05f; + } + + float wave(float x, float z) { + return sin(x * 0.1f + time) * cos(z * 0.1f + time) * 20; + } + + public static void main(String[] args) { + PApplet.disableAWT = true; + PApplet.main(AnimatedMesh.class.getName()); + } +} diff --git a/core/examples/src/main/java/webgpu/BackgroundImage.java b/core/examples/src/main/java/webgpu/BackgroundImage.java new file mode 100644 index 0000000000..d5386ae9c4 --- /dev/null +++ b/core/examples/src/main/java/webgpu/BackgroundImage.java @@ -0,0 +1,36 @@ +package webgpu; + +import processing.core.PApplet; +import processing.core.PImage; + +public class BackgroundImage extends PApplet { + + PImage img; + + public void settings() { + size(400, 400, WEBGPU); + } + + public void setup() { + img = createImage(400, 400, RGB); + img.loadPixels(); + for (int y = 0; y < img.height; y++) { + for (int x = 0; x < img.width; x++) { + int r = (int) (x * 255.0 / img.width); + int g = (int) (y * 255.0 / img.height); + int b = 128; + img.pixels[y * img.width + x] = color(r, g, b); + } + } + img.updatePixels(); + } + + public void draw() { + background(img); + } + + public static void main(String[] args) { + PApplet.disableAWT = true; + PApplet.main(BackgroundImage.class.getName()); + } +} diff --git a/core/examples/src/main/java/webgpu/Box3D.java b/core/examples/src/main/java/webgpu/Box3D.java new file mode 100644 index 0000000000..2f24b9a93c --- /dev/null +++ b/core/examples/src/main/java/webgpu/Box3D.java @@ -0,0 +1,35 @@ +package webgpu; + +import processing.core.PApplet; + +public class Box3D extends PApplet { + + float angle = 0; + + public void settings() { + size(400, 400, WEBGPU); + } + + public void setup() { + perspective(PI/3, (float)width/height, 0.1f, 1000); + camera(200, 200, 300, 0, 0, 0, 0, 1, 0); + } + + public void draw() { + background(26, 26, 38); + + pushMatrix(); + rotateY(angle); + rotateX(angle * 0.7f); + fill(200, 100, 100); + box(100); + popMatrix(); + + angle += 0.02f; + } + + public static void main(String[] args) { + PApplet.disableAWT = true; + PApplet.main(Box3D.class.getName()); + } +} diff --git a/core/examples/src/main/java/webgpu/Rectangle.java b/core/examples/src/main/java/webgpu/Rectangle.java new file mode 100644 index 0000000000..5a9b2c61d1 --- /dev/null +++ b/core/examples/src/main/java/webgpu/Rectangle.java @@ -0,0 +1,23 @@ +package webgpu; + +import processing.core.PApplet; + +public class Rectangle extends PApplet { + + public void settings() { + size(400, 400, WEBGPU); + } + + public void draw() { + background(51); + + fill(255); + noStroke(); + rect(10, 10, 100, 100); + } + + public static void main(String[] args) { + PApplet.disableAWT = true; + PApplet.main(Rectangle.class.getName()); + } +} diff --git a/core/examples/src/main/java/webgpu/Transforms.java b/core/examples/src/main/java/webgpu/Transforms.java new file mode 100644 index 0000000000..781423cd62 --- /dev/null +++ b/core/examples/src/main/java/webgpu/Transforms.java @@ -0,0 +1,47 @@ +package webgpu; + +import processing.core.PApplet; + +public class Transforms extends PApplet { + + float t = 0; + + public void settings() { + size(400, 400, WEBGPU); + } + + public void draw() { + background(26); + + noStroke(); + + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + pushMatrix(); + + translate(50 + j * 100, 50 + i * 100); + + float angle = t + (i + j) * PI / 8.0f; + rotate(angle); + + float s = 0.8f + sin(t * 2.0f + (i * j)) * 0.2f; + scale(s, s); + + float r = j / 3.0f; + float g = i / 3.0f; + fill(r * 255, g * 255, 204); + + rect(-20, -20, 40, 40); + + popMatrix(); + } + } + + t += 0.02f; + } + + public static void main(String[] args) { + PApplet.disableAWT = true; + PApplet.main(Transforms.class.getName()); + } +} diff --git a/core/examples/src/main/java/webgpu/UpdatePixels.java b/core/examples/src/main/java/webgpu/UpdatePixels.java new file mode 100644 index 0000000000..9ef2cf5aba --- /dev/null +++ b/core/examples/src/main/java/webgpu/UpdatePixels.java @@ -0,0 +1,74 @@ +package webgpu; + +import processing.core.PApplet; + +public class UpdatePixels extends PApplet { + + static final int RECT_W = 10; + static final int RECT_H = 10; + + boolean firstFrame = true; + + public void settings() { + size(100, 100, WEBGPU); + } + + public void draw() { + background(0); // Black background + + loadPixels(); + + for (int y = 20; y < 20 + RECT_H; y++) { + for (int x = 20; x < 20 + RECT_W; x++) { + pixels[y * width + x] = color(255, 0, 0); + } + } + + for (int y = 60; y < 60 + RECT_H; y++) { + for (int x = 60; x < 60 + RECT_W; x++) { + pixels[y * width + x] = color(0, 0, 255); + } + } + + updatePixels(); + + if (firstFrame) { + firstFrame = false; + + println("Total pixels: " + pixels.length); + + for (int y = 0; y < height; y++) { + StringBuilder row = new StringBuilder(); + for (int x = 0; x < width; x++) { + int idx = y * width + x; + int pixel = pixels[idx]; + float r = red(pixel); + float b = blue(pixel); + float a = alpha(pixel); + + if (r > 127) { + row.append("R"); + } else if (b > 127) { + row.append("B"); + } else if (a > 127) { + row.append("."); + } else { + row.append(" "); + } + } + println(row.toString()); + } + + println("\nSample pixels:"); + println("(25, 25): " + hex(pixels[25 * width + 25])); + println("(65, 65): " + hex(pixels[65 * width + 65])); + println("(0, 0): " + hex(pixels[0])); + println("(50, 50): " + hex(pixels[50 * width + 50])); + } + } + + public static void main(String[] args) { + PApplet.disableAWT = true; + PApplet.main(UpdatePixels.class.getName()); + } +} diff --git a/core/src/processing/core/NativeLibrary.java b/core/src/processing/core/NativeLibrary.java new file mode 100644 index 0000000000..208eeedd0c --- /dev/null +++ b/core/src/processing/core/NativeLibrary.java @@ -0,0 +1,112 @@ +package processing.core; + +import java.io.*; +import java.nio.file.*; + +/** + * Handles loading of Processing's native Rust library (libprocessing). + */ +public class NativeLibrary { + private static boolean loaded = false; + private static Throwable loadError = null; + + private static final String LIBRARY_NAME = "processing"; + + // Platform + private static final String OS_NAME = System.getProperty("os.name").toLowerCase(); + private static final String OS_ARCH = System.getProperty("os.arch").toLowerCase(); + + private static final String platform; + private static final String architecture; + private static final String libraryExtension; + + static { + // platform + if (OS_NAME.contains("mac") || OS_NAME.contains("darwin")) { + platform = "macos"; + libraryExtension = "dylib"; + } else if (OS_NAME.contains("win")) { + platform = "windows"; + libraryExtension = "dll"; + } else if (OS_NAME.contains("linux")) { + platform = "linux"; + libraryExtension = "so"; + } else { + throw new UnsupportedOperationException("Unsupported OS: " + OS_NAME); + } + + // architecture + if (OS_ARCH.contains("aarch64") || OS_ARCH.contains("arm")) { + architecture = "aarch64"; + } else if (OS_ARCH.contains("x86_64") || OS_ARCH.contains("amd64")) { + architecture = "x86_64"; + } else { + throw new UnsupportedOperationException("Unsupported architecture: " + OS_ARCH); + } + + // Load the dll + try { + loadNativeLibrary(); + loaded = true; + } catch (Throwable e) { + loadError = e; + System.err.println("Warning: Failed to load Processing native library: " + e.getMessage()); + } + } + + /** + * Ensures the native library is loaded. Throws if loading failed. + */ + public static void ensureLoaded() { + if (!loaded) { + throw new RuntimeException("Native library failed to load", loadError); + } + } + + /** + * Returns whether the native library was successfully loaded. + */ + public static boolean isLoaded() { + return loaded; + } + + /** + * Returns the platform string (e.g., "macos-aarch64"). + */ + public static String getPlatform() { + return platform + "-" + architecture; + } + + /** + * Extracts and loads the native library from JAR resources. + */ + private static void loadNativeLibrary() throws IOException { + String platformTarget = platform + "-" + architecture; + String libraryFileName = platform.equals("windows") + ? LIBRARY_NAME + "." + libraryExtension + : "lib" + LIBRARY_NAME + "." + libraryExtension; + String resourcePath = "/native/" + platformTarget + "/" + libraryFileName; + + // check classloader for resource in jar + InputStream libraryStream = NativeLibrary.class.getResourceAsStream(resourcePath); + if (libraryStream == null) { + throw new FileNotFoundException( + "Native library not found in JAR: " + resourcePath + + " (platform: " + platformTarget + ")" + ); + } + + // extract + Path tempDir = Files.createTempDirectory("processing-native-"); + tempDir.toFile().deleteOnExit(); + + Path libraryPath = tempDir.resolve(libraryFileName); + Files.copy(libraryStream, libraryPath, StandardCopyOption.REPLACE_EXISTING); + libraryStream.close(); + + libraryPath.toFile().deleteOnExit(); + + // load! + System.load(libraryPath.toAbsolutePath().toString()); + } +} diff --git a/core/src/processing/core/PApplet.java b/core/src/processing/core/PApplet.java index d9df211eb7..f6a247ba54 100644 --- a/core/src/processing/core/PApplet.java +++ b/core/src/processing/core/PApplet.java @@ -2008,8 +2008,8 @@ protected PGraphics createPrimaryGraphics() { * @see PGraphics */ public PImage createImage(int w, int h, int format) { - PImage image = new PImage(w, h, format); - image.parent = this; // make save() work + PImage image = (g != null) ? g.createImage(w, h, format) : new PImage(w, h, format); + image.parent = this; return image; } @@ -9983,10 +9983,6 @@ static public void runSketch(final String[] args, } break; - case ARGS_DISABLE_AWT: - disableAWT = true; - break; - case ARGS_WINDOW_COLOR: if (value.charAt(0) == '#' && value.length() == 7) { value = value.substring(1); @@ -10038,6 +10034,10 @@ static public void runSketch(final String[] args, fullScreen = true; break; + case ARGS_DISABLE_AWT: + disableAWT = true; + break; + default: name = args[argIndex]; break label; // because of break, argIndex won't increment again diff --git a/core/src/processing/core/PConstants.java b/core/src/processing/core/PConstants.java index d21a1fa49d..d1179984ee 100644 --- a/core/src/processing/core/PConstants.java +++ b/core/src/processing/core/PConstants.java @@ -72,6 +72,8 @@ public interface PConstants { // Experimental JavaFX renderer; even better 2D performance String FX2D = "processing.javafx.PGraphicsFX2D"; + String WEBGPU = "processing.webgpu.PGraphicsWebGPU"; + String PDF = "processing.pdf.PGraphicsPDF"; String SVG = "processing.svg.PGraphicsSVG"; String DXF = "processing.dxf.RawDXF"; diff --git a/core/src/processing/core/PGraphics.java b/core/src/processing/core/PGraphics.java index 1ada6aa2ae..8d411abef2 100644 --- a/core/src/processing/core/PGraphics.java +++ b/core/src/processing/core/PGraphics.java @@ -3830,6 +3830,11 @@ private void smoothWarning(String method) { // IMAGE + public PImage createImage(int w, int h, int format) { + return new PImage(w, h, format); + } + + /** * * Modifies the location from which images are drawn by changing the way in diff --git a/core/src/processing/webgpu/PGraphicsWebGPU.java b/core/src/processing/webgpu/PGraphicsWebGPU.java new file mode 100644 index 0000000000..ecbcbcb780 --- /dev/null +++ b/core/src/processing/webgpu/PGraphicsWebGPU.java @@ -0,0 +1,541 @@ +package processing.webgpu; + +import processing.core.PGraphics; +import processing.core.PImage; +import processing.core.PShape; +import processing.core.PSurface; + +import java.util.ArrayList; +import java.util.List; + +public class PGraphicsWebGPU extends PGraphics { + protected long surfaceId = 0; + private long graphicsId = 0; + + private long currentGeometry = 0; + private int shapeKind = 0; + private float normalX = 0, normalY = 0, normalZ = 1; + + private final List pendingDestroy = new ArrayList<>(); + + + @Override + public PSurface createSurface() { + return surface = new PSurfaceGLFW(this); + } + + protected void initWebGPUSurface(long windowHandle, long displayHandle, int width, int height, float scaleFactor) { + surfaceId = PWebGPU.createSurface(windowHandle, displayHandle, width, height, scaleFactor); + if (surfaceId == 0) { + System.err.println("Failed to create WebGPU surface"); + return; + } + graphicsId = PWebGPU.graphicsCreate(surfaceId, width, height); + if (graphicsId == 0) { + System.err.println("Failed to create WebGPU graphics context"); + } + } + + public long getSurfaceId() { + return surfaceId; + } + + @Override + public void setSize(int w, int h) { + super.setSize(w, h); + if (surfaceId != 0) { + PWebGPU.windowResized(surfaceId, pixelWidth, pixelHeight); + } + } + + @Override + public void beginDraw() { + super.beginDraw(); + if (graphicsId == 0) { + return; + } + PWebGPU.beginDraw(graphicsId); + checkSettings(); + } + + @Override + public void flush() { + super.flush(); + if (graphicsId == 0) { + return; + } + PWebGPU.flush(graphicsId); + + for (long geometryId : pendingDestroy) { + PWebGPU.geometryDestroy(geometryId); + } + pendingDestroy.clear(); + } + + @Override + public void endDraw() { + super.endDraw(); + if (graphicsId == 0) { + return; + } + PWebGPU.endDraw(graphicsId); + } + + @Override + public void dispose() { + super.dispose(); + if (surfaceId != 0) { + PWebGPU.destroySurface(surfaceId); + surfaceId = 0; + } + PWebGPU.exit(); + } + + // ── Background ────────────────────────────────────────────────────── + + @Override + protected void backgroundImpl() { + if (graphicsId == 0) { + return; + } + PWebGPU.backgroundColor(graphicsId, backgroundR, backgroundG, backgroundB, backgroundA); + } + + @Override + protected void backgroundImpl(PImage image) { + if (graphicsId == 0) { + return; + } + if (!(image instanceof PImageWebGPU)) { + throw new RuntimeException("WebGPU renderer requires PImageWebGPU. Use createImage()."); + } + PImageWebGPU img = (PImageWebGPU) image; + if (img.getId() == 0) { + img.loadPixels(); + byte[] rgba = pixelsToRGBA(img.pixels); + long imageId = PWebGPU.imageCreate(img.pixelWidth, img.pixelHeight, rgba); + img.setId(imageId); + } + PWebGPU.backgroundImage(graphicsId, img.getId()); + } + + // ── Fill / stroke ─────────────────────────────────────────────────── + + @Override + protected void fillFromCalc() { + super.fillFromCalc(); + if (graphicsId == 0) { + return; + } + if (fill) { + PWebGPU.setFill(graphicsId, fillR, fillG, fillB, fillA); + } else { + PWebGPU.noFill(graphicsId); + } + } + + @Override + protected void strokeFromCalc() { + super.strokeFromCalc(); + if (graphicsId == 0) { + return; + } + if (stroke) { + PWebGPU.setStrokeColor(graphicsId, strokeR, strokeG, strokeB, strokeA); + } else { + PWebGPU.noStroke(graphicsId); + } + } + + @Override + public void strokeWeight(float weight) { + super.strokeWeight(weight); + if (graphicsId == 0) { + return; + } + PWebGPU.setStrokeWeight(graphicsId, weight); + } + + @Override + public void noFill() { + super.noFill(); + if (graphicsId == 0) { + return; + } + PWebGPU.noFill(graphicsId); + } + + @Override + public void noStroke() { + super.noStroke(); + if (graphicsId == 0) { + return; + } + PWebGPU.noStroke(graphicsId); + } + + @Override + public void strokeCap(int cap) { + super.strokeCap(cap); + if (graphicsId == 0) { + return; + } + byte nativeCap = switch (cap) { + case ROUND -> PWebGPU.STROKE_CAP_ROUND; + case SQUARE -> PWebGPU.STROKE_CAP_SQUARE; + case PROJECT -> PWebGPU.STROKE_CAP_PROJECT; + default -> PWebGPU.STROKE_CAP_ROUND; + }; + PWebGPU.setStrokeCap(graphicsId, nativeCap); + } + + @Override + public void strokeJoin(int join) { + super.strokeJoin(join); + if (graphicsId == 0) { + return; + } + byte nativeJoin = switch (join) { + case ROUND -> PWebGPU.STROKE_JOIN_ROUND; + case MITER -> PWebGPU.STROKE_JOIN_MITER; + case BEVEL -> PWebGPU.STROKE_JOIN_BEVEL; + default -> PWebGPU.STROKE_JOIN_ROUND; + }; + PWebGPU.setStrokeJoin(graphicsId, nativeJoin); + } + + // ── Blend mode ────────────────────────────────────────────────────── + + @Override + public void blendMode(int mode) { + super.blendMode(mode); + if (graphicsId == 0) { + return; + } + byte nativeMode = switch (mode) { + case BLEND -> PWebGPU.BLEND_MODE_BLEND; + case ADD -> PWebGPU.BLEND_MODE_ADD; + case SUBTRACT -> PWebGPU.BLEND_MODE_SUBTRACT; + case DARKEST -> PWebGPU.BLEND_MODE_DARKEST; + case LIGHTEST -> PWebGPU.BLEND_MODE_LIGHTEST; + case DIFFERENCE -> PWebGPU.BLEND_MODE_DIFFERENCE; + case EXCLUSION -> PWebGPU.BLEND_MODE_EXCLUSION; + case MULTIPLY -> PWebGPU.BLEND_MODE_MULTIPLY; + case SCREEN -> PWebGPU.BLEND_MODE_SCREEN; + case REPLACE -> PWebGPU.BLEND_MODE_REPLACE; + default -> PWebGPU.BLEND_MODE_BLEND; + }; + PWebGPU.setBlendMode(graphicsId, nativeMode); + } + + // ── 2D primitives ─────────────────────────────────────────────────── + + @Override + protected void rectImpl(float x1, float y1, float x2, float y2) { + rectImpl(x1, y1, x2, y2, 0, 0, 0, 0); + } + + @Override + protected void rectImpl(float x1, float y1, float x2, float y2, + float tl, float tr, float br, float bl) { + if (graphicsId == 0) { + return; + } + PWebGPU.rect(graphicsId, x1, y1, x2 - x1, y2 - y1, tl, tr, br, bl); + } + + @Override + protected void ellipseImpl(float a, float b, float c, float d) { + if (graphicsId == 0) { + return; + } + PWebGPU.ellipse(graphicsId, a, b, c, d); + } + + @Override + protected void arcImpl(float a, float b, float c, float d, + float start, float stop, int mode) { + if (graphicsId == 0) { + return; + } + PWebGPU.arc(graphicsId, a, b, c, d, start, stop, (byte) mode); + } + + @Override + public void line(float x1, float y1, float x2, float y2) { + if (graphicsId == 0) { + return; + } + PWebGPU.line(graphicsId, x1, y1, x2, y2); + } + + @Override + public void point(float x, float y) { + if (graphicsId == 0) { + return; + } + PWebGPU.point(graphicsId, x, y); + } + + @Override + public void triangle(float x1, float y1, float x2, float y2, float x3, float y3) { + if (graphicsId == 0) { + return; + } + PWebGPU.triangle(graphicsId, x1, y1, x2, y2, x3, y3); + } + + @Override + public void quad(float x1, float y1, float x2, float y2, + float x3, float y3, float x4, float y4) { + if (graphicsId == 0) { + return; + } + PWebGPU.quad(graphicsId, x1, y1, x2, y2, x3, y3, x4, y4); + } + + // ── Curves ────────────────────────────────────────────────────────── + + @Override + public void bezier(float x1, float y1, float x2, float y2, + float x3, float y3, float x4, float y4) { + if (graphicsId == 0) { + return; + } + PWebGPU.bezier(graphicsId, x1, y1, x2, y2, x3, y3, x4, y4); + } + + @Override + public void curve(float x1, float y1, float x2, float y2, + float x3, float y3, float x4, float y4) { + if (graphicsId == 0) { + return; + } + PWebGPU.curve(graphicsId, x1, y1, x2, y2, x3, y3, x4, y4); + } + + // ── 3D shapes ─────────────────────────────────────────────────────── + + @Override + public void box(float w, float h, float d) { + if (graphicsId == 0) { + return; + } + long boxGeometry = PWebGPU.geometryBox(w, h, d); + PWebGPU.model(graphicsId, boxGeometry); + pendingDestroy.add(boxGeometry); + } + + @Override + public void sphere(float r) { + if (graphicsId == 0) { + return; + } + long sphereGeometry = PWebGPU.geometrySphere(r, sphereDetailU, sphereDetailV); + PWebGPU.model(graphicsId, sphereGeometry); + pendingDestroy.add(sphereGeometry); + } + + // ── Vertex shapes ─────────────────────────────────────────────────── + + @Override + public void beginShape(int kind) { + super.beginShape(kind); + if (graphicsId == 0) { + return; + } + shapeKind = kind; + byte topology = shapeKindToTopology(kind); + currentGeometry = PWebGPU.geometryCreate(topology); + } + + private byte shapeKindToTopology(int kind) { + return switch (kind) { + case POINTS -> PWebGPU.TOPOLOGY_POINT_LIST; + case LINES -> PWebGPU.TOPOLOGY_LINE_LIST; + case LINE_STRIP -> PWebGPU.TOPOLOGY_LINE_STRIP; + case TRIANGLES -> PWebGPU.TOPOLOGY_TRIANGLE_LIST; + case TRIANGLE_STRIP -> PWebGPU.TOPOLOGY_TRIANGLE_STRIP; + case TRIANGLE_FAN, QUADS, QUAD_STRIP, POLYGON -> PWebGPU.TOPOLOGY_TRIANGLE_LIST; + default -> PWebGPU.TOPOLOGY_TRIANGLE_LIST; + }; + } + + @Override + public void normal(float nx, float ny, float nz) { + normalX = nx; + normalY = ny; + normalZ = nz; + } + + @Override + public void vertex(float x, float y) { + vertex(x, y, 0); + } + + @Override + public void vertex(float x, float y, float z) { + if (currentGeometry == 0) { + return; + } + PWebGPU.geometryColor(currentGeometry, fillR, fillG, fillB, fillA); + PWebGPU.geometryNormal(currentGeometry, normalX, normalY, normalZ); + PWebGPU.geometryVertex(currentGeometry, x, y, z); + } + + @Override + public void endShape(int mode) { + if (graphicsId == 0 || currentGeometry == 0) { + return; + } + + if (shapeKind == QUADS) { + int vertexCount = PWebGPU.geometryVertexCount(currentGeometry); + for (int i = 0; i < vertexCount; i += 4) { + PWebGPU.geometryIndex(currentGeometry, i); + PWebGPU.geometryIndex(currentGeometry, i + 1); + PWebGPU.geometryIndex(currentGeometry, i + 2); + PWebGPU.geometryIndex(currentGeometry, i); + PWebGPU.geometryIndex(currentGeometry, i + 2); + PWebGPU.geometryIndex(currentGeometry, i + 3); + } + } + + PWebGPU.model(graphicsId, currentGeometry); + pendingDestroy.add(currentGeometry); + currentGeometry = 0; + } + + // ── Transform matrix ──────────────────────────────────────────────── + + @Override + public void pushMatrix() { + if (graphicsId == 0) { + return; + } + PWebGPU.pushMatrix(graphicsId); + } + + @Override + public void popMatrix() { + if (graphicsId == 0) { + return; + } + PWebGPU.popMatrix(graphicsId); + } + + @Override + public void resetMatrix() { + if (graphicsId == 0) { + return; + } + PWebGPU.resetMatrix(graphicsId); + } + + @Override + public void translate(float x, float y) { + if (graphicsId == 0) { + return; + } + PWebGPU.translate(graphicsId, x, y); + } + + @Override + public void rotate(float angle) { + if (graphicsId == 0) { + return; + } + PWebGPU.rotate(graphicsId, angle); + } + + @Override + public void scale(float x, float y) { + if (graphicsId == 0) { + return; + } + PWebGPU.scale(graphicsId, x, y); + } + + @Override + public void shearX(float angle) { + if (graphicsId == 0) { + return; + } + PWebGPU.shearX(graphicsId, angle); + } + + @Override + public void shearY(float angle) { + if (graphicsId == 0) { + return; + } + PWebGPU.shearY(graphicsId, angle); + } + + // ── 3D camera / projection ────────────────────────────────────────── + + @Override + public void camera(float eyeX, float eyeY, float eyeZ, + float centerX, float centerY, float centerZ, + float upX, float upY, float upZ) { + if (graphicsId == 0) { + return; + } + PWebGPU.mode3d(graphicsId); + // TODO: camera up vector is not yet exposed by libprocessing FFI + } + + @Override + public void perspective(float fov, float aspect, float near, float far) { + if (graphicsId == 0) { + return; + } + PWebGPU.mode3d(graphicsId); + PWebGPU.perspective(graphicsId, fov, aspect, near, far); + } + + @Override + public void ortho(float left, float right, float bottom, float top, float near, float far) { + if (graphicsId == 0) { + return; + } + PWebGPU.ortho(graphicsId, left, right, bottom, top, near, far); + } + + // ── Images / shapes ───────────────────────────────────────────────── + + public PImageWebGPU createImage(int width, int height, int format) { + return new PImageWebGPU(width, height, format); + } + + @Override + public PShape createShape() { + return new PShapeWebGPU(this, PShape.GEOMETRY); + } + + @Override + public PShape createShape(int type) { + return new PShapeWebGPU(this, type); + } + + public void model(long geometryId) { + if (graphicsId == 0) { + return; + } + PWebGPU.model(graphicsId, geometryId); + } + + // ── Helpers ────────────────────────────────────────────────────────── + + private byte[] pixelsToRGBA(int[] pixels) { + byte[] rgba = new byte[pixels.length * 4]; + for (int i = 0; i < pixels.length; i++) { + int pixel = pixels[i]; + rgba[i * 4] = (byte) ((pixel >> 16) & 0xFF); + rgba[i * 4 + 1] = (byte) ((pixel >> 8) & 0xFF); + rgba[i * 4 + 2] = (byte) (pixel & 0xFF); + rgba[i * 4 + 3] = (byte) ((pixel >> 24) & 0xFF); + } + return rgba; + } +} diff --git a/core/src/processing/webgpu/PImageWebGPU.java b/core/src/processing/webgpu/PImageWebGPU.java new file mode 100644 index 0000000000..d60d6530aa --- /dev/null +++ b/core/src/processing/webgpu/PImageWebGPU.java @@ -0,0 +1,28 @@ +package processing.webgpu; + +import processing.core.PImage; + +public class PImageWebGPU extends PImage { + + protected long id = 0; + + public PImageWebGPU() { + super(); + } + + public PImageWebGPU(int width, int height) { + super(width, height); + } + + public PImageWebGPU(int width, int height, int format) { + super(width, height, format); + } + + public long getId() { + return id; + } + + public void setId(long imageId) { + this.id = imageId; + } +} diff --git a/core/src/processing/webgpu/PShapeWebGPU.java b/core/src/processing/webgpu/PShapeWebGPU.java new file mode 100644 index 0000000000..3e25a7add9 --- /dev/null +++ b/core/src/processing/webgpu/PShapeWebGPU.java @@ -0,0 +1,273 @@ +package processing.webgpu; + +import processing.core.PShape; +import processing.core.PVector; + +/** + * WebGPU implementation of PShape. + */ +public class PShapeWebGPU extends PShape { + + /** Reference to the graphics context */ + protected PGraphicsWebGPU pg; + + /** Native geometry ID (0 = not yet created) */ + protected long geometryId = 0; + + /** Native layout ID for custom vertex attributes */ + protected long layoutId = 0; + + /** Current topology for this shape */ + protected byte topology = PWebGPU.TOPOLOGY_TRIANGLE_LIST; + + /** Track if we're currently building the shape */ + protected boolean building = false; + + /** Pending normal for next vertex */ + protected float normalX, normalY, normalZ; + protected boolean hasNormal = false; + + /** Pending color for next vertex */ + protected float colorR, colorG, colorB, colorA; + protected boolean hasColor = false; + + /** Pending UV for next vertex */ + protected float uvU, uvV; + protected boolean hasUV = false; + + /** + * Create a new WebGPU shape. + */ + public PShapeWebGPU(PGraphicsWebGPU pg, int family) { + this.pg = pg; + this.family = family; + } + + /** + * Map Processing shape kinds to WebGPU topologies. + */ + protected byte kindToTopology(int kind) { + return switch (kind) { + case POINTS -> PWebGPU.TOPOLOGY_POINT_LIST; + case LINES -> PWebGPU.TOPOLOGY_LINE_LIST; + case LINE_STRIP -> PWebGPU.TOPOLOGY_LINE_STRIP; + case TRIANGLES -> PWebGPU.TOPOLOGY_TRIANGLE_LIST; + case TRIANGLE_STRIP -> PWebGPU.TOPOLOGY_TRIANGLE_STRIP; + case TRIANGLE_FAN -> PWebGPU.TOPOLOGY_TRIANGLE_LIST; // Will need tessellation + case QUADS -> PWebGPU.TOPOLOGY_TRIANGLE_LIST; // Will need tessellation + case QUAD_STRIP -> PWebGPU.TOPOLOGY_TRIANGLE_STRIP; + default -> PWebGPU.TOPOLOGY_TRIANGLE_LIST; + }; + } + + @Override + public void beginShape(int kind) { + this.kind = kind; + this.topology = kindToTopology(kind); + + // Create or reset the geometry + if (geometryId != 0) { + PWebGPU.geometryDestroy(geometryId); + } + geometryId = PWebGPU.geometryCreate(topology); + building = true; + + // Reset pending attributes + hasNormal = false; + hasColor = false; + hasUV = false; + } + + @Override + public void endShape(int mode) { + building = false; + // If CLOSE mode and we have a polygon, we might need to add indices + // For now, the geometry is ready to be rendered + } + + @Override + public void normal(float nx, float ny, float nz) { + if (geometryId != 0) { + PWebGPU.geometryNormal(geometryId, nx, ny, nz); + } + normalX = nx; + normalY = ny; + normalZ = nz; + hasNormal = true; + } + + @Override + public void vertex(float x, float y) { + vertex(x, y, 0); + } + + @Override + public void vertex(float x, float y, float z) { + if (geometryId == 0) { + return; + } + if (hasColor) { + PWebGPU.geometryColor(geometryId, colorR, colorG, colorB, colorA); + } + if (hasUV) { + PWebGPU.geometryUv(geometryId, uvU, uvV); + } + PWebGPU.geometryVertex(geometryId, x, y, z); + } + + @Override + public void vertex(float x, float y, float u, float v) { + texture(u, v); + vertex(x, y, 0); + } + + @Override + public void vertex(float x, float y, float z, float u, float v) { + texture(u, v); + vertex(x, y, z); + } + + /** + * Set texture coordinates for the next vertex. + */ + public void texture(float u, float v) { + uvU = u; + uvV = v; + hasUV = true; + } + + /** + * Set the fill color for subsequent vertices. + */ + @Override + public void fill(int rgb) { + colorR = ((rgb >> 16) & 0xFF) / 255f; + colorG = ((rgb >> 8) & 0xFF) / 255f; + colorB = (rgb & 0xFF) / 255f; + colorA = ((rgb >> 24) & 0xFF) / 255f; + hasColor = true; + } + + @Override + public void fill(float gray) { + colorR = colorG = colorB = gray / 255f; + colorA = 1f; + hasColor = true; + } + + @Override + public void fill(float r, float g, float b) { + colorR = r / 255f; + colorG = g / 255f; + colorB = b / 255f; + colorA = 1f; + hasColor = true; + } + + @Override + public void fill(float r, float g, float b, float a) { + colorR = r / 255f; + colorG = g / 255f; + colorB = b / 255f; + colorA = a / 255f; + hasColor = true; + } + + /** + * Add an index for indexed rendering. + */ + public void index(int i) { + if (geometryId != 0) { + PWebGPU.geometryIndex(geometryId, i); + } + } + + @Override + public int getVertexCount() { + if (geometryId == 0) { + return 0; + } + return PWebGPU.geometryVertexCount(geometryId); + } + + /** + * Get the index count for this shape. + */ + public int getIndexCount() { + if (geometryId == 0) { + return 0; + } + return PWebGPU.geometryIndexCount(geometryId); + } + + @Override + public void setVertex(int index, float x, float y, float z) { + if (geometryId != 0) { + PWebGPU.geometrySetVertex(geometryId, index, x, y, z); + } + } + + @Override + public void setNormal(int index, float nx, float ny, float nz) { + if (geometryId != 0) { + PWebGPU.geometrySetNormal(geometryId, index, nx, ny, nz); + } + } + + /** + * Set the color of a specific vertex. + */ + public void setColor(int index, float r, float g, float b, float a) { + if (geometryId != 0) { + PWebGPU.geometrySetColor(geometryId, index, r, g, b, a); + } + } + + /** + * Set the UV coordinates of a specific vertex. + */ + public void setUv(int index, float u, float v) { + if (geometryId != 0) { + PWebGPU.geometrySetUv(geometryId, index, u, v); + } + } + + /** + * Get the native geometry ID for direct rendering. + */ + public long getGeometryId() { + return geometryId; + } + + /** + * Draw this shape using the associated graphics context. + */ + public void draw() { + if (geometryId != 0 && pg != null) { + pg.model(geometryId); + } + } + + /** + * Release native resources. + */ + public void dispose() { + if (geometryId != 0) { + PWebGPU.geometryDestroy(geometryId); + geometryId = 0; + } + if (layoutId != 0) { + PWebGPU.geometryLayoutDestroy(layoutId); + layoutId = 0; + } + } + + /** + * Create a box geometry. + */ + public static PShapeWebGPU createBox(PGraphicsWebGPU pg, float width, float height, float depth) { + PShapeWebGPU shape = new PShapeWebGPU(pg, GEOMETRY); + shape.geometryId = PWebGPU.geometryBox(width, height, depth); + return shape; + } +} diff --git a/core/src/processing/webgpu/PSurfaceGLFW.java b/core/src/processing/webgpu/PSurfaceGLFW.java new file mode 100644 index 0000000000..49cecdb636 --- /dev/null +++ b/core/src/processing/webgpu/PSurfaceGLFW.java @@ -0,0 +1,534 @@ +package processing.webgpu; + +import org.lwjgl.glfw.*; +import org.lwjgl.system.MemoryUtil; +import org.lwjgl.system.Platform; + +import processing.core.PApplet; +import processing.core.PGraphics; +import processing.core.PImage; +import processing.core.PSurface; + +import java.io.File; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public class PSurfaceGLFW implements PSurface { + + protected PApplet sketch; + protected PGraphics graphics; + + protected long window; + protected long display; + protected boolean running = false; + + protected boolean paused; + private final Lock pauseLock = new ReentrantLock(); + private final Condition pauseCondition = pauseLock.newCondition(); + + protected float frameRateTarget = 60; + protected long frameRatePeriod = 1000000000L / 60L; + + private static final AtomicInteger windowCount = new AtomicInteger(0); + private static AtomicBoolean glfwInitialized = new AtomicBoolean(false); + + private GLFWFramebufferSizeCallback framebufferSizeCallback; + private GLFWWindowPosCallback windowPosCallback; + private GLFWCursorPosCallback cursorPosCallback; + private GLFWMouseButtonCallback mouseButtonCallback; + private GLFWScrollCallback scrollCallback; + private GLFWKeyCallback keyCallback; + private GLFWCharCallback charCallback; + private GLFWCursorEnterCallback cursorEnterCallback; + private GLFWWindowFocusCallback windowFocusCallback; + + public PSurfaceGLFW(PGraphics graphics) { + this.graphics = graphics; + } + + @Override + public void initOffscreen(PApplet sketch) { + throw new IllegalStateException("PSurfaceGLFW does not support offscreen rendering"); + } + + @Override + public void initFrame(PApplet sketch) { + this.sketch = sketch; + + if (glfwInitialized.compareAndSet(false, true)) { + GLFWErrorCallback.createPrint(System.err).set(); + if (!GLFW.glfwInit()) { + glfwInitialized.set(false); + throw new IllegalStateException("Failed to initialize GLFW"); + } + System.out.println("PSurfaceGLFW: GLFW initialized successfully"); + } + + GLFW.glfwDefaultWindowHints(); + GLFW.glfwWindowHint(GLFW.GLFW_CLIENT_API, GLFW.GLFW_NO_API); + GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE); + GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_FALSE); + + window = GLFW.glfwCreateWindow(sketch.sketchWidth(), sketch.sketchHeight(), "Processing", + MemoryUtil.NULL, MemoryUtil.NULL); + if (window == MemoryUtil.NULL) { + throw new RuntimeException("Failed to create GLFW window"); + } + + display = GLFW.glfwGetPrimaryMonitor(); + + windowCount.incrementAndGet(); + + initListeners(); + + if (graphics instanceof PGraphicsWebGPU webgpu) { + PWebGPU.init(); + + long windowHandle = getWindowHandle(); + long displayHandle = getDisplayHandle(); + int width = sketch.sketchWidth(); + int height = sketch.sketchHeight(); + float scaleFactor = sketch.sketchPixelDensity(); + + webgpu.initWebGPUSurface(windowHandle, displayHandle, width, height, scaleFactor); + } + } + + protected void initListeners() { + long surfaceId = getSurfaceId(); + + framebufferSizeCallback = GLFW.glfwSetFramebufferSizeCallback(window, + (window, width, height) -> { + if (sketch != null) { + sketch.postWindowResized(width, height); + } + }); + + windowPosCallback = GLFW.glfwSetWindowPosCallback(window, + (window, xpos, ypos) -> { + if (sketch != null) { + sketch.postWindowMoved(xpos, ypos); + } + }); + + cursorPosCallback = GLFW.glfwSetCursorPosCallback(window, + (window, xpos, ypos) -> { + if (surfaceId != 0) { + PWebGPU.inputMouseMove(surfaceId, (float) xpos, (float) ypos); + } + }); + + mouseButtonCallback = GLFW.glfwSetMouseButtonCallback(window, + (window, button, action, mods) -> { + if (surfaceId != 0) { + byte btn = switch (button) { + case GLFW.GLFW_MOUSE_BUTTON_LEFT -> 0; + case GLFW.GLFW_MOUSE_BUTTON_MIDDLE -> 1; + case GLFW.GLFW_MOUSE_BUTTON_RIGHT -> 2; + default -> -1; + }; + if (btn >= 0) { + PWebGPU.inputMouseButton(surfaceId, btn, action == GLFW.GLFW_PRESS); + } + } + }); + + scrollCallback = GLFW.glfwSetScrollCallback(window, + (window, xoffset, yoffset) -> { + if (surfaceId != 0) { + PWebGPU.inputScroll(surfaceId, (float) xoffset, (float) yoffset); + } + }); + + keyCallback = GLFW.glfwSetKeyCallback(window, + (window, key, scancode, action, mods) -> { + if (surfaceId != 0 && action != GLFW.GLFW_REPEAT) { + PWebGPU.inputKey(surfaceId, key, action == GLFW.GLFW_PRESS); + } + }); + + charCallback = GLFW.glfwSetCharCallback(window, + (window, codepoint) -> { + // char callback doesn't give us a key code, so pass 0 + if (surfaceId != 0) { + PWebGPU.inputChar(surfaceId, 0, codepoint); + } + }); + + cursorEnterCallback = GLFW.glfwSetCursorEnterCallback(window, + (window, entered) -> { + if (surfaceId != 0) { + if (entered) { + PWebGPU.inputCursorEnter(surfaceId); + } else { + PWebGPU.inputCursorLeave(surfaceId); + } + } + }); + + windowFocusCallback = GLFW.glfwSetWindowFocusCallback(window, + (window, focused) -> { + if (surfaceId != 0) { + PWebGPU.inputFocus(surfaceId, focused); + } + }); + } + + private long getSurfaceId() { + if (graphics instanceof PGraphicsWebGPU webgpu) { + return webgpu.getSurfaceId(); + } + return 0; + } + + @Override + public Object getNative() { + return window; + } + + public long getWindowHandle() { + if (Platform.get() == Platform.MACOSX) { + return GLFWNativeCocoa.glfwGetCocoaWindow(window); + } else if (Platform.get() == Platform.WINDOWS) { + return GLFWNativeWin32.glfwGetWin32Window(window); + } else if (Platform.get() == Platform.LINUX) { + // TODO: need to check if x11 or wayland + return GLFWNativeWayland.glfwGetWaylandWindow(window); + } else { + throw new UnsupportedOperationException("Window handle retrieval not implemented for this platform"); + } + } + + public long getDisplayHandle() { + if (Platform.get() == Platform.MACOSX) { + return 0; + } else if (Platform.get() == Platform.WINDOWS) { + return 0; + } else if (Platform.get() == Platform.LINUX) { + // TODO: need to check if x11 or wayland + return GLFWNativeWayland.glfwGetWaylandDisplay(); + } else { + throw new UnsupportedOperationException("Window handle retrieval not implemented for this platform"); + } + } + + @Override + public void setTitle(String title) { + if (window != MemoryUtil.NULL) { + GLFW.glfwSetWindowTitle(window, title); + } + } + + @Override + public void setVisible(boolean visible) { + if (window != MemoryUtil.NULL) { + if (visible) { + GLFW.glfwShowWindow(window); + } else { + GLFW.glfwHideWindow(window); + } + } + } + + @Override + public void setResizable(boolean resizable) { + if (window != MemoryUtil.NULL) { + GLFW.glfwSetWindowAttrib(window, GLFW.GLFW_RESIZABLE, + resizable ? GLFW.GLFW_TRUE : GLFW.GLFW_FALSE); + } + } + + @Override + public void setAlwaysOnTop(boolean always) { + if (window != MemoryUtil.NULL) { + GLFW.glfwSetWindowAttrib(window, GLFW.GLFW_FLOATING, + always ? GLFW.GLFW_TRUE : GLFW.GLFW_FALSE); + } + } + + @Override + public void setIcon(PImage icon) { + // TODO: set icon with glfw + } + + @Override + public void placeWindow(int[] location, int[] editorLocation) { + if (window == MemoryUtil.NULL) return; + + int x, y; + if (location != null) { + x = location[0]; + y = location[1]; + } else if (editorLocation != null) { + x = editorLocation[0] - 20; + y = editorLocation[1]; + + if (x - sketch.sketchWidth() < 10) { + long monitor = GLFW.glfwGetPrimaryMonitor(); + var vidmode = GLFW.glfwGetVideoMode(monitor); + if (vidmode != null) { + x = (vidmode.width() - sketch.sketchWidth()) / 2; + y = (vidmode.height() - sketch.sketchHeight()) / 2; + } else { + x = 100; + y = 100; + } + } + } else { + long monitor = GLFW.glfwGetPrimaryMonitor(); + var vidmode = GLFW.glfwGetVideoMode(monitor); + if (vidmode != null) { + x = (vidmode.width() - sketch.sketchWidth()) / 2; + y = (vidmode.height() - sketch.sketchHeight()) / 2; + } else { + x = 100; + y = 100; + } + } + + GLFW.glfwSetWindowPos(window, x, y); + } + + @Override + public void placePresent(int stopColor) { + // TODO: implement present mode support + } + + @Override + public void setLocation(int x, int y) { + if (window != MemoryUtil.NULL) { + GLFW.glfwSetWindowPos(window, x, y); + } + } + + @Override + public void setSize(int width, int height) { + if (width == sketch.width && height == sketch.height) { + return; + } + + sketch.width = width; + sketch.height = height; + graphics.setSize(width, height); + + if (window != MemoryUtil.NULL) { + GLFW.glfwSetWindowSize(window, width, height); + } + } + + @Override + public void setFrameRate(float fps) { + frameRateTarget = fps; + frameRatePeriod = (long) (1000000000.0 / frameRateTarget); + } + + @Override + public void setCursor(int kind) { + // TODO: implement cursor types + } + + @Override + public void setCursor(PImage image, int hotspotX, int hotspotY) { + // TODO: implement custom cursor + } + + @Override + public void showCursor() { + if (window != MemoryUtil.NULL) { + GLFW.glfwSetInputMode(window, GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL); + } + } + + @Override + public void hideCursor() { + if (window != MemoryUtil.NULL) { + GLFW.glfwSetInputMode(window, GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_HIDDEN); + } + } + + @Override + public PImage loadImage(String path, Object... args) { + // TODO: implement image loading without awt + throw new UnsupportedOperationException("Image loading not yet implemented for WebGPU"); + } + + @Override + public boolean openLink(String url) { + // TODO: implement links without awt + return false; + } + + @Override + public void selectInput(String prompt, String callback, File file, Object callbackObject) { + throw new UnsupportedOperationException("File dialogs not yet implemented for WebGPU"); + } + + @Override + public void selectOutput(String prompt, String callback, File file, Object callbackObject) { + throw new UnsupportedOperationException("File dialogs not yet implemented for WebGPU"); + } + + @Override + public void selectFolder(String prompt, String callback, File file, Object callbackObject) { + throw new UnsupportedOperationException("Folder selection not yet implemented for WebGPU"); + } + + @Override + public void startThread() { + if (running) { + throw new IllegalStateException("Draw loop already running"); + } + + running = true; + runDrawLoop(); + } + + protected void runDrawLoop() { + GLFW.glfwShowWindow(window); + + long beforeTime = System.nanoTime(); + long overSleepTime = 0L; + + sketch.start(); + + while (running) { + checkPause(); + + GLFW.glfwPollEvents(); + PWebGPU.inputFlush(); + + if (GLFW.glfwWindowShouldClose(window)) { + sketch.exit(); + break; + } + + if (!sketch.finished) { + sketch.handleDraw(); + } + + long afterTime = System.nanoTime(); + long timeDiff = afterTime - beforeTime; + long sleepTime = (frameRatePeriod - timeDiff) - overSleepTime; + + if (sleepTime > 0) { + try { + Thread.sleep(sleepTime / 1000000L, (int) (sleepTime % 1000000L)); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + overSleepTime = (System.nanoTime() - afterTime) - sleepTime; + } else { + overSleepTime = 0L; + } + + beforeTime = System.nanoTime(); + } + + sketch.dispose(); + } + + @Override + public void pauseThread() { + paused = true; + } + + protected void checkPause() { + if (paused) { + pauseLock.lock(); + try { + while (paused) { + pauseCondition.await(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + pauseLock.unlock(); + } + } + } + + @Override + public void resumeThread() { + pauseLock.lock(); + try { + paused = false; + pauseCondition.signalAll(); + } finally { + pauseLock.unlock(); + } + } + + @Override + public boolean stopThread() { + if (!running) { + return false; + } + + running = false; + + try { + if (window != MemoryUtil.NULL) { + GLFW.glfwDestroyWindow(window); + window = MemoryUtil.NULL; + + if (windowCount.decrementAndGet() == 0) { + if (glfwInitialized.compareAndSet(true, false)) { + GLFW.glfwTerminate(); + System.out.println("PSurfaceGLFW: GLFW terminated"); + } + } + } + } finally { + freeCallbacks(); + } + + return true; + } + + private void freeCallbacks() { + if (framebufferSizeCallback != null) { + framebufferSizeCallback.free(); + framebufferSizeCallback = null; + } + if (windowPosCallback != null) { + windowPosCallback.free(); + windowPosCallback = null; + } + if (cursorPosCallback != null) { + cursorPosCallback.free(); + cursorPosCallback = null; + } + if (mouseButtonCallback != null) { + mouseButtonCallback.free(); + mouseButtonCallback = null; + } + if (scrollCallback != null) { + scrollCallback.free(); + scrollCallback = null; + } + if (keyCallback != null) { + keyCallback.free(); + keyCallback = null; + } + if (charCallback != null) { + charCallback.free(); + charCallback = null; + } + if (cursorEnterCallback != null) { + cursorEnterCallback.free(); + cursorEnterCallback = null; + } + if (windowFocusCallback != null) { + windowFocusCallback.free(); + windowFocusCallback = null; + } + } + + @Override + public boolean isStopped() { + return !running; + } +} diff --git a/core/src/processing/webgpu/PWebGPU.java b/core/src/processing/webgpu/PWebGPU.java new file mode 100644 index 0000000000..fd3b5c751c --- /dev/null +++ b/core/src/processing/webgpu/PWebGPU.java @@ -0,0 +1,878 @@ +package processing.webgpu; + +import processing.core.NativeLibrary; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; + +import static java.lang.foreign.MemorySegment.NULL; +import static processing.ffi.processing_h.*; +import processing.ffi.Color; + +public class PWebGPU { + + static { + ensureLoaded(); + } + + public static void ensureLoaded() { + NativeLibrary.ensureLoaded(); + } + + // ── Init / lifecycle ──────────────────────────────────────────────── + + public static void init() { + processing_init(); + checkError(); + } + + public static void exit() { + processing_exit((byte) 0); + checkError(); + } + + // ── Surface ───────────────────────────────────────────────────────── + + public static long createSurface(long windowHandle, long displayHandle, int width, int height, float scaleFactor) { + long surfaceId = processing_surface_create(windowHandle, displayHandle, width, height, scaleFactor); + checkError(); + return surfaceId; + } + + public static void destroySurface(long surfaceId) { + processing_surface_destroy(surfaceId); + checkError(); + } + + public static void windowResized(long surfaceId, int width, int height) { + processing_surface_resize(surfaceId, width, height); + checkError(); + } + + // ── Graphics context ──────────────────────────────────────────────── + + public static long graphicsCreate(long surfaceId, int width, int height) { + long graphicsId = processing_graphics_create(surfaceId, width, height); + checkError(); + return graphicsId; + } + + public static void graphicsDestroy(long graphicsId) { + processing_graphics_destroy(graphicsId); + checkError(); + } + + public static void beginDraw(long graphicsId) { + processing_begin_draw(graphicsId); + checkError(); + } + + public static void flush(long graphicsId) { + processing_flush(graphicsId); + checkError(); + } + + public static void endDraw(long graphicsId) { + processing_end_draw(graphicsId); + checkError(); + } + + // ── Background ────────────────────────────────────────────────────── + + public static void backgroundColor(long graphicsId, float r, float g, float b, float a) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment color = allocateColor(arena, r, g, b, a); + processing_background_color(graphicsId, color); + checkError(); + } + } + + public static void backgroundImage(long graphicsId, long imageId) { + processing_background_image(graphicsId, imageId); + checkError(); + } + + // ── Color mode ────────────────────────────────────────────────────── + + public static final byte COLOR_SPACE_SRGB = 0; + public static final byte COLOR_SPACE_HSB = 1; + public static final byte COLOR_SPACE_LINEAR = 2; + + public static void colorMode(long graphicsId, byte space, float max1, float max2, float max3, float maxAlpha) { + processing_color_mode(graphicsId, space, max1, max2, max3, maxAlpha); + checkError(); + } + + // ── Fill / stroke ─────────────────────────────────────────────────── + + public static void setFill(long graphicsId, float r, float g, float b, float a) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment color = allocateColor(arena, r, g, b, a); + processing_set_fill(graphicsId, color); + checkError(); + } + } + + public static void setStrokeColor(long graphicsId, float r, float g, float b, float a) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment color = allocateColor(arena, r, g, b, a); + processing_set_stroke_color(graphicsId, color); + checkError(); + } + } + + public static void setStrokeWeight(long graphicsId, float weight) { + processing_set_stroke_weight(graphicsId, weight); + checkError(); + } + + public static void noFill(long graphicsId) { + processing_no_fill(graphicsId); + checkError(); + } + + public static void noStroke(long graphicsId) { + processing_no_stroke(graphicsId); + checkError(); + } + + // ── Stroke style ──────────────────────────────────────────────────── + + public static final byte STROKE_CAP_ROUND = 0; + public static final byte STROKE_CAP_SQUARE = 1; + public static final byte STROKE_CAP_PROJECT = 2; + + public static final byte STROKE_JOIN_ROUND = 0; + public static final byte STROKE_JOIN_MITER = 1; + public static final byte STROKE_JOIN_BEVEL = 2; + + public static void setStrokeCap(long graphicsId, byte cap) { + processing_set_stroke_cap(graphicsId, cap); + checkError(); + } + + public static void setStrokeJoin(long graphicsId, byte join) { + processing_set_stroke_join(graphicsId, join); + checkError(); + } + + // ── Shape modes ───────────────────────────────────────────────────── + + public static void rectMode(long graphicsId, byte mode) { + processing_rect_mode(graphicsId, mode); + checkError(); + } + + public static void ellipseMode(long graphicsId, byte mode) { + processing_ellipse_mode(graphicsId, mode); + checkError(); + } + + // ── Blend modes ───────────────────────────────────────────────────── + + public static final byte BLEND_MODE_BLEND = 0; + public static final byte BLEND_MODE_ADD = 1; + public static final byte BLEND_MODE_SUBTRACT = 2; + public static final byte BLEND_MODE_DARKEST = 3; + public static final byte BLEND_MODE_LIGHTEST = 4; + public static final byte BLEND_MODE_DIFFERENCE = 5; + public static final byte BLEND_MODE_EXCLUSION = 6; + public static final byte BLEND_MODE_MULTIPLY = 7; + public static final byte BLEND_MODE_SCREEN = 8; + public static final byte BLEND_MODE_REPLACE = 9; + + public static void setBlendMode(long graphicsId, byte mode) { + processing_set_blend_mode(graphicsId, mode); + checkError(); + } + + // ── 2D drawing matrix ─────────────────────────────────────────────── + + public static void pushMatrix(long graphicsId) { + processing_push_matrix(graphicsId); + checkError(); + } + + public static void popMatrix(long graphicsId) { + processing_pop_matrix(graphicsId); + checkError(); + } + + public static void resetMatrix(long graphicsId) { + processing_reset_matrix(graphicsId); + checkError(); + } + + public static void translate(long graphicsId, float x, float y) { + processing_translate(graphicsId, x, y); + checkError(); + } + + public static void rotate(long graphicsId, float angle) { + processing_rotate(graphicsId, angle); + checkError(); + } + + public static void scale(long graphicsId, float x, float y) { + processing_scale(graphicsId, x, y); + checkError(); + } + + public static void shearX(long graphicsId, float angle) { + processing_shear_x(graphicsId, angle); + checkError(); + } + + public static void shearY(long graphicsId, float angle) { + processing_shear_y(graphicsId, angle); + checkError(); + } + + // ── 2D primitives ─────────────────────────────────────────────────── + + public static void rect(long graphicsId, float x, float y, float w, float h, + float tl, float tr, float br, float bl) { + processing_rect(graphicsId, x, y, w, h, tl, tr, br, bl); + checkError(); + } + + public static void ellipse(long graphicsId, float cx, float cy, float w, float h) { + processing_ellipse(graphicsId, cx, cy, w, h); + checkError(); + } + + public static void circle(long graphicsId, float cx, float cy, float d) { + processing_circle(graphicsId, cx, cy, d); + checkError(); + } + + public static void line(long graphicsId, float x1, float y1, float x2, float y2) { + processing_line(graphicsId, x1, y1, x2, y2); + checkError(); + } + + public static void triangle(long graphicsId, float x1, float y1, float x2, float y2, + float x3, float y3) { + processing_triangle(graphicsId, x1, y1, x2, y2, x3, y3); + checkError(); + } + + public static void quad(long graphicsId, float x1, float y1, float x2, float y2, + float x3, float y3, float x4, float y4) { + processing_quad(graphicsId, x1, y1, x2, y2, x3, y3, x4, y4); + checkError(); + } + + public static void point(long graphicsId, float x, float y) { + processing_point(graphicsId, x, y); + checkError(); + } + + public static void square(long graphicsId, float x, float y, float s) { + processing_square(graphicsId, x, y, s); + checkError(); + } + + public static void arc(long graphicsId, float cx, float cy, float w, float h, + float start, float stop, byte mode) { + processing_arc(graphicsId, cx, cy, w, h, start, stop, mode); + checkError(); + } + + public static void bezier(long graphicsId, float x1, float y1, float x2, float y2, + float x3, float y3, float x4, float y4) { + processing_bezier(graphicsId, x1, y1, x2, y2, x3, y3, x4, y4); + checkError(); + } + + public static void curve(long graphicsId, float x1, float y1, float x2, float y2, + float x3, float y3, float x4, float y4) { + processing_curve(graphicsId, x1, y1, x2, y2, x3, y3, x4, y4); + checkError(); + } + + // ── 3D primitives ─────────────────────────────────────────────────── + + public static void cylinder(long graphicsId, float radius, float height, int detail) { + processing_cylinder(graphicsId, radius, height, detail); + checkError(); + } + + public static void cone(long graphicsId, float radius, float height, int detail) { + processing_cone(graphicsId, radius, height, detail); + checkError(); + } + + public static void torus(long graphicsId, float radius, float tubeRadius, int majorSegments, int minorSegments) { + processing_torus(graphicsId, radius, tubeRadius, majorSegments, minorSegments); + checkError(); + } + + public static void plane(long graphicsId, float width, float height) { + processing_plane(graphicsId, width, height); + checkError(); + } + + public static void capsule(long graphicsId, float radius, float length, int detail) { + processing_capsule(graphicsId, radius, length, detail); + checkError(); + } + + public static void conicalFrustum(long graphicsId, float radiusTop, float radiusBottom, float height, int detail) { + processing_conical_frustum(graphicsId, radiusTop, radiusBottom, height, detail); + checkError(); + } + + public static void tetrahedron(long graphicsId, float radius) { + processing_tetrahedron(graphicsId, radius); + checkError(); + } + + // ── Vertex shapes ─────────────────────────────────────────────────── + + public static void beginShape(long graphicsId, byte kind) { + processing_begin_shape(graphicsId, kind); + checkError(); + } + + public static void endShape(long graphicsId, boolean close) { + processing_end_shape(graphicsId, close); + checkError(); + } + + public static void shapeVertex(long graphicsId, float x, float y) { + processing_vertex(graphicsId, x, y); + checkError(); + } + + public static void bezierVertex(long graphicsId, float cx1, float cy1, float cx2, float cy2, float x, float y) { + processing_bezier_vertex(graphicsId, cx1, cy1, cx2, cy2, x, y); + checkError(); + } + + public static void quadraticVertex(long graphicsId, float cx, float cy, float x, float y) { + processing_quadratic_vertex(graphicsId, cx, cy, x, y); + checkError(); + } + + public static void curveVertex(long graphicsId, float x, float y) { + processing_curve_vertex(graphicsId, x, y); + checkError(); + } + + public static void beginContour(long graphicsId) { + processing_begin_contour(graphicsId); + checkError(); + } + + public static void endContour(long graphicsId) { + processing_end_contour(graphicsId); + checkError(); + } + + // ── 3D mode / projection ──────────────────────────────────────────── + + public static void mode3d(long graphicsId) { + processing_mode_3d(graphicsId); + checkError(); + } + + public static void mode2d(long graphicsId) { + processing_mode_2d(graphicsId); + checkError(); + } + + public static void perspective(long graphicsId, float fov, float aspect, float near, float far) { + processing_perspective(graphicsId, fov, aspect, near, far); + checkError(); + } + + public static void ortho(long graphicsId, float left, float right, float bottom, float top, float near, float far) { + processing_ortho(graphicsId, left, right, bottom, top, near, far); + checkError(); + } + + // ── Entity transforms (3D objects: lights, geometry, etc.) ────────── + + public static void transformSetPosition(long entityId, float x, float y, float z) { + processing_transform_set_position(entityId, x, y, z); + checkError(); + } + + public static void transformTranslate(long entityId, float x, float y, float z) { + processing_transform_translate(entityId, x, y, z); + checkError(); + } + + public static void transformSetRotation(long entityId, float x, float y, float z) { + processing_transform_set_rotation(entityId, x, y, z); + checkError(); + } + + public static void transformRotateX(long entityId, float angle) { + processing_transform_rotate_x(entityId, angle); + checkError(); + } + + public static void transformRotateY(long entityId, float angle) { + processing_transform_rotate_y(entityId, angle); + checkError(); + } + + public static void transformRotateZ(long entityId, float angle) { + processing_transform_rotate_z(entityId, angle); + checkError(); + } + + public static void transformRotateAxis(long entityId, float angle, float axisX, float axisY, float axisZ) { + processing_transform_rotate_axis(entityId, angle, axisX, axisY, axisZ); + checkError(); + } + + public static void transformSetScale(long entityId, float x, float y, float z) { + processing_transform_set_scale(entityId, x, y, z); + checkError(); + } + + public static void transformScale(long entityId, float x, float y, float z) { + processing_transform_scale(entityId, x, y, z); + checkError(); + } + + public static void transformLookAt(long entityId, float targetX, float targetY, float targetZ) { + processing_transform_look_at(entityId, targetX, targetY, targetZ); + checkError(); + } + + public static void transformReset(long entityId) { + processing_transform_reset(entityId); + checkError(); + } + + // ── Lights ────────────────────────────────────────────────────────── + + public static long lightCreateDirectional(long graphicsId, float r, float g, float b, float a, float illuminance) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment color = allocateColor(arena, r, g, b, a); + long id = processing_light_create_directional(graphicsId, color, illuminance); + checkError(); + return id; + } + } + + public static long lightCreatePoint(long graphicsId, float r, float g, float b, float a, + float intensity, float range, float radius) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment color = allocateColor(arena, r, g, b, a); + long id = processing_light_create_point(graphicsId, color, intensity, range, radius); + checkError(); + return id; + } + } + + public static long lightCreateSpot(long graphicsId, float r, float g, float b, float a, + float intensity, float range, float radius, + float innerAngle, float outerAngle) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment color = allocateColor(arena, r, g, b, a); + long id = processing_light_create_spot(graphicsId, color, intensity, range, radius, innerAngle, outerAngle); + checkError(); + return id; + } + } + + // ── Materials ─────────────────────────────────────────────────────── + + public static long materialCreatePbr() { + long id = processing_material_create_pbr(); + checkError(); + return id; + } + + public static void materialSetFloat(long matId, String name, float value) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment nameSegment = arena.allocateFrom(name); + processing_material_set_float(matId, nameSegment, value); + checkError(); + } + } + + public static void materialSetFloat4(long matId, String name, float r, float g, float b, float a) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment nameSegment = arena.allocateFrom(name); + processing_material_set_float4(matId, nameSegment, r, g, b, a); + checkError(); + } + } + + public static void materialDestroy(long matId) { + processing_material_destroy(matId); + checkError(); + } + + public static void material(long graphicsId, long matId) { + processing_material(graphicsId, matId); + checkError(); + } + + // ── Images ────────────────────────────────────────────────────────── + + public static long imageCreate(int width, int height, byte[] data) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment dataSegment = arena.allocateFrom(java.lang.foreign.ValueLayout.JAVA_BYTE, data); + long imageId = processing_image_create(width, height, dataSegment, data.length); + checkError(); + return imageId; + } + } + + public static long imageLoad(String path) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment pathSegment = arena.allocateFrom(path); + long imageId = processing_image_load(pathSegment); + checkError(); + return imageId; + } + } + + public static void imageResize(long imageId, int newWidth, int newHeight) { + processing_image_resize(imageId, newWidth, newHeight); + checkError(); + } + + public static void imageReadback(long imageId, float[] buffer) { + try (Arena arena = Arena.ofConfined()) { + int numPixels = buffer.length / 4; + MemorySegment colorBuffer = Color.allocateArray(numPixels, arena); + processing_image_readback(imageId, colorBuffer, numPixels); + checkError(); + + for (int i = 0; i < numPixels; i++) { + MemorySegment color = Color.asSlice(colorBuffer, i); + buffer[i * 4] = Color.c1(color); + buffer[i * 4 + 1] = Color.c2(color); + buffer[i * 4 + 2] = Color.c3(color); + buffer[i * 4 + 3] = Color.a(color); + } + } + } + + // ── Input: event ingestion (called by PSurfaceGLFW) ───────────────── + + public static void inputMouseMove(long surfaceId, float x, float y) { + processing_input_mouse_move(surfaceId, x, y); + checkError(); + } + + public static void inputMouseButton(long surfaceId, byte button, boolean pressed) { + processing_input_mouse_button(surfaceId, button, pressed); + checkError(); + } + + public static void inputScroll(long surfaceId, float x, float y) { + processing_input_scroll(surfaceId, x, y); + checkError(); + } + + public static void inputKey(long surfaceId, int keyCode, boolean pressed) { + processing_input_key(surfaceId, keyCode, pressed); + checkError(); + } + + public static void inputChar(long surfaceId, int keyCode, int codepoint) { + processing_input_char(surfaceId, keyCode, codepoint); + checkError(); + } + + public static void inputCursorEnter(long surfaceId) { + processing_input_cursor_enter(surfaceId); + checkError(); + } + + public static void inputCursorLeave(long surfaceId) { + processing_input_cursor_leave(surfaceId); + checkError(); + } + + public static void inputFocus(long surfaceId, boolean focused) { + processing_input_focus(surfaceId, focused); + checkError(); + } + + public static void inputFlush() { + processing_input_flush(); + checkError(); + } + + // ── Input: state queries ──────────────────────────────────────────── + + public static float mouseX(long surfaceId) { + return processing_mouse_x(surfaceId); + } + + public static float mouseY(long surfaceId) { + return processing_mouse_y(surfaceId); + } + + public static float pmouseX(long surfaceId) { + return processing_pmouse_x(surfaceId); + } + + public static float pmouseY(long surfaceId) { + return processing_pmouse_y(surfaceId); + } + + public static boolean mouseIsPressed() { + return processing_mouse_is_pressed(); + } + + public static byte mouseButton() { + return processing_mouse_button(); + } + + public static boolean keyIsPressed() { + return processing_key_is_pressed(); + } + + public static boolean keyIsDown(int keyCode) { + return processing_key_is_down(keyCode); + } + + public static boolean keyJustPressed(int keyCode) { + return processing_key_just_pressed(keyCode); + } + + public static int key() { + return processing_key(); + } + + public static int keyCode() { + return processing_key_code(); + } + + public static float movedX() { + return processing_moved_x(); + } + + public static float movedY() { + return processing_moved_y(); + } + + public static float mouseWheel() { + return processing_mouse_wheel(); + } + + // ── Geometry ──────────────────────────────────────────────────────── + + public static final byte TOPOLOGY_POINT_LIST = 0; + public static final byte TOPOLOGY_LINE_LIST = 1; + public static final byte TOPOLOGY_LINE_STRIP = 2; + public static final byte TOPOLOGY_TRIANGLE_LIST = 3; + public static final byte TOPOLOGY_TRIANGLE_STRIP = 4; + + public static final byte ATTR_FORMAT_FLOAT = 1; + public static final byte ATTR_FORMAT_FLOAT2 = 2; + public static final byte ATTR_FORMAT_FLOAT3 = 3; + public static final byte ATTR_FORMAT_FLOAT4 = 4; + + public static long geometryLayoutCreate() { + long layoutId = processing_geometry_layout_create(); + checkError(); + return layoutId; + } + + public static void geometryLayoutAddPosition(long layoutId) { + processing_geometry_layout_add_position(layoutId); + checkError(); + } + + public static void geometryLayoutAddNormal(long layoutId) { + processing_geometry_layout_add_normal(layoutId); + checkError(); + } + + public static void geometryLayoutAddColor(long layoutId) { + processing_geometry_layout_add_color(layoutId); + checkError(); + } + + public static void geometryLayoutAddUv(long layoutId) { + processing_geometry_layout_add_uv(layoutId); + checkError(); + } + + public static void geometryLayoutAddAttribute(long layoutId, long attrId) { + processing_geometry_layout_add_attribute(layoutId, attrId); + checkError(); + } + + public static void geometryLayoutDestroy(long layoutId) { + processing_geometry_layout_destroy(layoutId); + checkError(); + } + + public static long geometryCreate(byte topology) { + long geoId = processing_geometry_create(topology); + checkError(); + return geoId; + } + + public static long geometryCreateWithLayout(long layoutId, byte topology) { + long geoId = processing_geometry_create_with_layout(layoutId, topology); + checkError(); + return geoId; + } + + public static long geometryBox(float width, float height, float depth) { + long geoId = processing_geometry_box(width, height, depth); + checkError(); + return geoId; + } + + public static long geometrySphere(float radius, int sectors, int stacks) { + long geoId = processing_geometry_sphere(radius, sectors, stacks); + checkError(); + return geoId; + } + + public static void geometryDestroy(long geoId) { + processing_geometry_destroy(geoId); + checkError(); + } + + public static void geometryNormal(long geoId, float nx, float ny, float nz) { + processing_geometry_normal(geoId, nx, ny, nz); + checkError(); + } + + public static void geometryColor(long geoId, float r, float g, float b, float a) { + processing_geometry_color(geoId, r, g, b, a); + checkError(); + } + + public static void geometryUv(long geoId, float u, float v) { + processing_geometry_uv(geoId, u, v); + checkError(); + } + + public static void geometryVertex(long geoId, float x, float y, float z) { + processing_geometry_vertex(geoId, x, y, z); + checkError(); + } + + public static void geometryIndex(long geoId, int i) { + processing_geometry_index(geoId, i); + checkError(); + } + + public static long geometryAttributeCreate(String name, byte format) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment nameSegment = arena.allocateFrom(name); + long attrId = processing_geometry_attribute_create(nameSegment, format); + checkError(); + return attrId; + } + } + + public static void geometryAttributeDestroy(long attrId) { + processing_geometry_attribute_destroy(attrId); + checkError(); + } + + public static long geometryAttributePosition() { + return processing_geometry_attribute_position(); + } + + public static long geometryAttributeNormal() { + return processing_geometry_attribute_normal(); + } + + public static long geometryAttributeColor() { + return processing_geometry_attribute_color(); + } + + public static long geometryAttributeUv() { + return processing_geometry_attribute_uv(); + } + + public static void geometryAttributeFloat(long geoId, long attrId, float v) { + processing_geometry_attribute_float(geoId, attrId, v); + checkError(); + } + + public static void geometryAttributeFloat2(long geoId, long attrId, float x, float y) { + processing_geometry_attribute_float2(geoId, attrId, x, y); + checkError(); + } + + public static void geometryAttributeFloat3(long geoId, long attrId, float x, float y, float z) { + processing_geometry_attribute_float3(geoId, attrId, x, y, z); + checkError(); + } + + public static void geometryAttributeFloat4(long geoId, long attrId, float x, float y, float z, float w) { + processing_geometry_attribute_float4(geoId, attrId, x, y, z, w); + checkError(); + } + + public static int geometryVertexCount(long geoId) { + int count = processing_geometry_vertex_count(geoId); + checkError(); + return count; + } + + public static int geometryIndexCount(long geoId) { + int count = processing_geometry_index_count(geoId); + checkError(); + return count; + } + + public static void geometrySetVertex(long geoId, int index, float x, float y, float z) { + processing_geometry_set_vertex(geoId, index, x, y, z); + checkError(); + } + + public static void geometrySetNormal(long geoId, int index, float nx, float ny, float nz) { + processing_geometry_set_normal(geoId, index, nx, ny, nz); + checkError(); + } + + public static void geometrySetColor(long geoId, int index, float r, float g, float b, float a) { + processing_geometry_set_color(geoId, index, r, g, b, a); + checkError(); + } + + public static void geometrySetUv(long geoId, int index, float u, float v) { + processing_geometry_set_uv(geoId, index, u, v); + checkError(); + } + + public static void model(long graphicsId, long geoId) { + processing_model(graphicsId, geoId); + checkError(); + } + + // ── Helpers ────────────────────────────────────────────────────────── + + private static MemorySegment allocateColor(Arena arena, float r, float g, float b, float a) { + MemorySegment color = Color.allocate(arena); + Color.c1(color, r); + Color.c2(color, g); + Color.c3(color, b); + Color.a(color, a); + Color.space(color, COLOR_SPACE_SRGB); + return color; + } + + private static void checkError() { + MemorySegment ret = processing_check_error(); + if (ret.equals(NULL)) { + return; + } + + String errorMsg = ret.getString(0); + if (errorMsg != null && !errorMsg.isEmpty()) { + throw new PWebGPUException(errorMsg); + } + } +} diff --git a/core/src/processing/webgpu/PWebGPUException.java b/core/src/processing/webgpu/PWebGPUException.java new file mode 100644 index 0000000000..b3907e0653 --- /dev/null +++ b/core/src/processing/webgpu/PWebGPUException.java @@ -0,0 +1,13 @@ +package processing.webgpu; + +/** + * Unchecked exception thrown for WebGPU-related errors. + *

+ * WebGPU operations can fail for various reasons, such as unsupported hardware, but are not + * expected to be recoverable by the Processing application. + */ +public class PWebGPUException extends RuntimeException { + public PWebGPUException(String message) { + super(message); + } +} diff --git a/core/test/processing/webgpu/PWebGPUTest.java b/core/test/processing/webgpu/PWebGPUTest.java new file mode 100644 index 0000000000..405e70e1c4 --- /dev/null +++ b/core/test/processing/webgpu/PWebGPUTest.java @@ -0,0 +1,13 @@ +package processing.webgpu; + +import org.junit.Test; + +/** + * Tests for the PWebGPU native interface. + */ +public class PWebGPUTest { + @Test + public void itLoads() { + PWebGPU.ensureLoaded(); + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 49d41db2ad..4455f92044 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin = "2.2.20" +kotlin = "2.3.21" compose-plugin = "1.9.1" jogl = "2.6.0" antlr = "4.13.2" diff --git a/gradle/plugins/library/src/main/kotlin/ProcessingLibraryPlugin.kt b/gradle/plugins/library/src/main/kotlin/ProcessingLibraryPlugin.kt index 4514f581fd..d1df93f75e 100644 --- a/gradle/plugins/library/src/main/kotlin/ProcessingLibraryPlugin.kt +++ b/gradle/plugins/library/src/main/kotlin/ProcessingLibraryPlugin.kt @@ -36,8 +36,9 @@ class ProcessingLibraryPlugin : Plugin { target.dependencies.add("compileOnly", "org.processing:core:$processingVersion") } } + val javaVersionOverride = target.findProperty("enableWebGPU")?.toString()?.toBoolean()?.let { if (it) 24 else 17 } ?: 17 target.extensions.configure(JavaPluginExtension::class.java) { extension -> - extension.toolchain.languageVersion.set(JavaLanguageVersion.of(17)) + extension.toolchain.languageVersion.set(JavaLanguageVersion.of(javaVersionOverride)) } target.plugins.withType(JavaPlugin::class.java) { diff --git a/gradle/plugins/settings.gradle.kts b/gradle/plugins/settings.gradle.kts index ab39f6aca7..dc4f98668e 100644 --- a/gradle/plugins/settings.gradle.kts +++ b/gradle/plugins/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } include("library") \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 94113f200e..2e1113280e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/java/src/processing/mode/java/JavaBuild.java b/java/src/processing/mode/java/JavaBuild.java index b696ab0e20..af802c66ff 100644 --- a/java/src/processing/mode/java/JavaBuild.java +++ b/java/src/processing/mode/java/JavaBuild.java @@ -66,6 +66,7 @@ public class JavaBuild { private boolean foundMain = false; private String classPath; protected String sketchClassName; + protected String sketchRenderer; /** * This will include the code folder, any library folders, etc. that might @@ -118,6 +119,7 @@ public String build(File srcFolder, File binFolder, boolean sizeWarning) throws // that will bubble up to whomever called build(). if (Compiler.compile(this)) { sketchClassName = classNameFound; + sketchRenderer = result.getSketchRenderer(); return classNameFound; } return null; @@ -128,6 +130,10 @@ public String getSketchClassName() { return sketchClassName; } + public String getSketchRenderer() { + return sketchRenderer; + } + /** * Build all the code for this sketch. diff --git a/java/src/processing/mode/java/PreprocService.java b/java/src/processing/mode/java/PreprocService.java index 410cff02f6..a8a4a0c1c2 100644 --- a/java/src/processing/mode/java/PreprocService.java +++ b/java/src/processing/mode/java/PreprocService.java @@ -667,7 +667,7 @@ private void setupParser(boolean resolveBindings, String className, if (resolveBindings) { parser.setUnitName(className); - parser.setEnvironment(classPathArray, null, null, false); + parser.setEnvironment(classPathArray, null, null, true); parser.setResolveBindings(true); } } diff --git a/java/src/processing/mode/java/runner/Runner.java b/java/src/processing/mode/java/runner/Runner.java index a9baf1ea6a..d0bded04de 100644 --- a/java/src/processing/mode/java/runner/Runner.java +++ b/java/src/processing/mode/java/runner/Runner.java @@ -342,6 +342,10 @@ protected StringList getMachineParams() { // No longer needed / doesn't seem to do anything differently //params.append("-Dcom.apple.mrj.application.apple.menu.about.name=" + // build.getSketchClassName()); + + if ("WEBGPU".equals(build.getSketchRenderer())) { + params.append("-XstartOnFirstThread"); + } } /* if (Platform.isWindows()) { @@ -380,6 +384,10 @@ protected StringList getMachineParams() { // http://processing.org/bugs/bugzilla/1188.html params.append("-ea"); + // we need to open up access to internal jdk modules for libraries that use reflection + // this will break at some point in the future when these modules are removed from the jdk :( + params.append("--enable-native-access=ALL-UNNAMED"); + return params; } @@ -508,6 +516,9 @@ protected StringList getSketchParams(boolean present, String[] args) { } */ + // TODO: excise AWT to make webgpu work properly + params.append(PApplet.ARGS_DISABLE_AWT); + params.append(build.getSketchClassName()); } // Add command-line arguments to be given to the sketch itself diff --git a/libprocessing b/libprocessing new file mode 160000 index 0000000000..a9f0774597 --- /dev/null +++ b/libprocessing @@ -0,0 +1 @@ +Subproject commit a9f0774597a81afe2cfccac07c99f81eada253e6