Compare commits

...

2 commits

Author SHA1 Message Date
e19ee6d867
A lot 2024-08-28 20:15:57 +02:00
2891fd73ed
A lot 2024-08-24 10:52:08 +02:00
157 changed files with 2224 additions and 22271 deletions

56
.gitignore vendored
View file

@ -1,56 +0,0 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### IntelliJ IDEA ###
/.idea/workspace.xml
/.idea/usage.statistics.xml
/.idea/uiDesigner.xml
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### OTHER ###
/wiki
.DS_Store
*.bat
*.conf
*.dll
*.dylib
*.jar
*.jpg
*.mhr
*.ogg
*.sh
*.so
*.ttf
*.wav
*.zip
touch.txt

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="16" />
</component>
</project>

View file

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View file

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
</component>
</project>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_16" project-jdk-name="corretto-16" project-jdk-type="JavaSDK" />
</project>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

7
Engine/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
.gradle/
gradle/
gradlew
gradlew.bat
build/
.idea/

53
Engine/build.gradle.kts Normal file
View file

@ -0,0 +1,53 @@
plugins {
java
id("com.gradleup.shadow") version "8.3.0"
}
subprojects {
apply(plugin = "java")
apply(plugin = "com.gradleup.shadow")
java {
toolchain {
languageVersion = JavaLanguageVersion.of(22)
}
withSourcesJar()
withJavadocJar()
}
}
val lwjglVersion = "3.3.4"
val jomlVersion = "1.10.7"
val jomlPrimitivesVersion = "1.10.0"
val lwjglNatives = Pair(
System.getProperty("os.name")!!,
System.getProperty("os.arch")!!
).let { (name, arch) ->
when {
arrayOf("Linux", "SunOS", "Unit").any { name.startsWith(it) } ->
if (arrayOf("arm", "aarch64").any { arch.startsWith(it) })
"natives-linux${if (arch.contains("64") || arch.startsWith("armv8")) "-arm64" else "-arm32"}"
else if (arch.startsWith("ppc"))
"natives-linux-ppc64le"
else if (arch.startsWith("riscv"))
"natives-linux-riscv64"
else
"natives-linux"
arrayOf("Mac OS X", "Darwin").any { name.startsWith(it) } ->
"natives-macos${if (arch.startsWith("aarch64")) "-arm64" else ""}"
arrayOf("Windows").any { name.startsWith(it) } ->
if (arch.contains("64"))
"natives-windows${if (arch.startsWith("aarch64")) "-arm64" else ""}"
else
"natives-windows-x86"
else ->
throw Error("Unrecognized or unsupported platform. Please set \"lwjglNatives\" manually")
}
}
extra["lwjglVersion"] = lwjglVersion
extra["jomlVersion"] = jomlVersion
extra["jomlPrimitivesVersion"] = jomlPrimitivesVersion
extra["lwjglNatives"] = lwjglNatives

View file

@ -0,0 +1,7 @@
rootProject.name = "Engine"
include(":core")
project(":core").projectDir = file("src/core")
include(":opengl")
project(":opengl").projectDir = file("src/opengl")
include(":rendering")
project(":rendering").projectDir = file("src/rendering")

View file

@ -0,0 +1,59 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
repositories {
mavenCentral()
}
plugins {
java
id("com.gradleup.shadow") version "8.3.0"
}
val lwjglVersion: String by rootProject.extra
val jomlVersion: String by rootProject.extra
val jomlPrimitivesVersion: String by rootProject.extra
val lwjglNatives: String by rootProject.extra
sourceSets {
val main by getting {
java.srcDir("java")
}
}
dependencies {
annotationProcessor("org.projectlombok:lombok:1.18.34")
implementation("org.projectlombok:lombok:1.18.34")
annotationProcessor("javax.annotation:javax.annotation-api:1.3.2")
implementation("javax.annotation:javax.annotation-api:1.3.2")
implementation(platform("org.lwjgl:lwjgl-bom:$lwjglVersion"))
implementation("org.lwjgl", "lwjgl")
runtimeOnly("org.lwjgl", "lwjgl", classifier = lwjglNatives)
implementation("org.lwjgl", "lwjgl-glfw")
runtimeOnly("org.lwjgl", "lwjgl-glfw", classifier = lwjglNatives)
implementation("org.lwjgl", "lwjgl-stb")
runtimeOnly("org.lwjgl", "lwjgl-stb", classifier = lwjglNatives)
implementation("org.joml", "joml", jomlVersion)
implementation("org.joml", "joml-primitives", jomlPrimitivesVersion)
shadow(localGroovy())
shadow(gradleApi())
//implementation("org.lwjgl", "lwjgl-nuklear")
//implementation("org.lwjgl", "lwjgl-openal")
//implementation("org.lwjgl", "lwjgl-remotery")
//runtimeOnly("org.lwjgl", "lwjgl-nuklear", classifier = lwjglNatives)
//runtimeOnly("org.lwjgl", "lwjgl-openal", classifier = lwjglNatives)
//runtimeOnly("org.lwjgl", "lwjgl-remotery", classifier = lwjglNatives)
}
tasks.named<ShadowJar>("shadowJar") {
archiveFileName.set("dev.euph.engine.core.jar")
}
configurations {
shadow
}

View file

@ -0,0 +1,37 @@
package dev.euph.engine.core.data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Getter
@EqualsAndHashCode
public class Archetype<T> {
private final Set<Class<? extends T>> componentClasses;
@SafeVarargs
public Archetype(Class<? extends T>... componentClasses) {
this.componentClasses = new HashSet<>(Arrays.asList(componentClasses));
}
public Archetype(Set<Class<? extends T>> componentClasses) {
this.componentClasses = new HashSet<>(componentClasses);
}
public Archetype(List<Class<? extends T>> componentClasses) {
this.componentClasses = new HashSet<>(componentClasses);
}
public boolean contains(Class<? extends T> componentClass) {
for (Class<? extends T> _componentClass : componentClasses) {
if (componentClass.isAssignableFrom(_componentClass)) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,27 @@
package dev.euph.engine.core.data;
import org.lwjgl.BufferUtils;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import static org.lwjgl.stb.STBImage.*;
public class ImageLoader {
public record Image(ByteBuffer data, int width, int height) {
}
public static Image loadImage(String path) {
IntBuffer width = BufferUtils.createIntBuffer(1);
IntBuffer height = BufferUtils.createIntBuffer(1);
IntBuffer channels = BufferUtils.createIntBuffer(1);
ByteBuffer imageBuffer = stbi_load(path, width, height, channels, 0);
if(imageBuffer == null) {
throw new RuntimeException("Failed to load image: " + stbi_failure_reason());
}
Image image = new Image(imageBuffer, width.get(0), height.get(0));
stbi_image_free(imageBuffer);
return image;
}
}

View file

@ -1,16 +1,14 @@
package dev.euph.engine.datastructs.octree;
package dev.euph.engine.core.data.octree;
import lombok.AllArgsConstructor;
import org.joml.Vector3f;
import java.util.List;
@AllArgsConstructor
public class Octree<T> {
private final OctreeNode<T> root;
public Octree(int maxCapacity, Vector3f center, float halfSize) {
root = new OctreeNode<T>(maxCapacity, center, halfSize);
}
public void insert(Vector3f position, T data) {
root.insert(position, data);
}

View file

@ -1,5 +1,6 @@
package dev.euph.engine.datastructs.octree;
package dev.euph.engine.core.data.octree;
import lombok.Getter;
import org.joml.Vector3f;
import java.util.ArrayList;
@ -7,15 +8,13 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static java.lang.Math.max;
import static java.lang.String.join;
public class OctreeNode<T> {
private final int maxCapacity;
private final Vector3f center;
private final float halfSize;
private final List<Vector3f> positions;
private final Map<Vector3f, T> dataMap;
@Getter
private OctreeNode<T>[] children;
public OctreeNode(int maxCapacity, Vector3f center, float halfSize) {
@ -28,27 +27,23 @@ public class OctreeNode<T> {
}
public void insert(Vector3f position, T data) {
// Check if the item fits within this node's region
if (isOutOfBounds(position)) {
return;
}
// If the node is not subdivided, add the item to its data list and map
if (children == null) {
positions.add(position);
dataMap.put(position, data);
// Subdivide if the capacity is exceeded
if (positions.size() > maxCapacity) {
subdivide();
}
} else {
// Otherwise, insert the item into the appropriate child node
return;
}
for (OctreeNode<T> child : children) {
child.insert(position, data);
}
}
}
public List<T> query(Vector3f position, float radius) {
List<T> result = new ArrayList<>();
@ -57,21 +52,20 @@ public class OctreeNode<T> {
return result;
}
// Add data items from this node if their positions are within the query radius
for (Vector3f itemPosition : positions) {
if (itemPosition.distance(position) <= radius) {
result.add(dataMap.get(itemPosition));
}
}
// Recursively query child nodes
if (children != null) {
if (children == null) {
return result;
}
for (OctreeNode<T> child : children) {
result.addAll(child.query(position, radius));
}
}
return result;
}
private boolean isOutOfBounds(Vector3f position) {
@ -98,7 +92,6 @@ public class OctreeNode<T> {
float quarterSize = halfSize / 2.0f;
// Calculate centers for each child node
Vector3f[] childCenters = new Vector3f[]{
new Vector3f(center.x - quarterSize, center.y - quarterSize, center.z - quarterSize),
new Vector3f(center.x + quarterSize, center.y - quarterSize, center.z - quarterSize),
@ -110,18 +103,14 @@ public class OctreeNode<T> {
new Vector3f(center.x + quarterSize, center.y + quarterSize, center.z + quarterSize)
};
// Create and initialize child nodes
for (int i = 0; i < 8; i++) {
childNodes[i] = new OctreeNode<>(maxCapacity, childCenters[i], quarterSize);
}
children = childNodes; // Assign the initialized children array to the class field
children = childNodes;
// Redistribute positions and data from this node to children
for (int i = positions.size() - 1; i >= 0; i--) {
Vector3f position = positions.get(i);
T data = dataMap.remove(position); // Remove data from the current node
// Find the child that contains the position and add data to it
T data = dataMap.remove(position);
for (OctreeNode<T> child : children) {
if (!child.isOutOfBounds(position)) {
child.insert(position, data);
@ -129,7 +118,6 @@ public class OctreeNode<T> {
}
}
// Remove the position from the current node
positions.remove(i);
}
}
@ -146,8 +134,4 @@ public class OctreeNode<T> {
return allPositions;
}
public OctreeNode<T>[] getChildren() {
return children;
}
}

View file

@ -1,4 +1,4 @@
package dev.euph.engine.datastructs.pipeline;
package dev.euph.engine.core.data.pipeline;
import java.util.ArrayList;
import java.util.Collection;
@ -10,12 +10,15 @@ public class Pipeline<Input, Output> {
public Pipeline() {
this.pipelineStages = new ArrayList<>();
}
public Pipeline(PipelineStage<Input, Output> pipelineStage) {
pipelineStages = Collections.singletonList(pipelineStage);
}
private Pipeline(Collection<PipelineStage<?, ?>> pipelineStages) {
this.pipelineStages = new ArrayList<>(pipelineStages);
}
public <NewOutput> Pipeline<Input, NewOutput> addStage(PipelineStage<Output, NewOutput> pipelineStage) {
final ArrayList<PipelineStage<?, ?>> newPipelineStages = new ArrayList<>(pipelineStages);
newPipelineStages.add(pipelineStage);

View file

@ -1,15 +1,18 @@
package dev.euph.engine.datastructs.pipeline;
package dev.euph.engine.core.data.pipeline;
public class PipelineRuntimeException extends RuntimeException {
public PipelineRuntimeException(String message) {
super("Pipeline Runtime Exception: " + message);
}
public PipelineRuntimeException(String message, Throwable cause) {
super("Pipeline Runtime Exception: " + message, cause);
}
public PipelineRuntimeException(Throwable cause) {
super("Pipeline Runtime Exception: " + cause);
}
protected PipelineRuntimeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super("Pipeline Runtime Exception: " + message, cause, enableSuppression, writableStackTrace);
}

View file

@ -1,4 +1,4 @@
package dev.euph.engine.datastructs.pipeline;
package dev.euph.engine.core.data.pipeline;
public interface PipelineStage<Input, Output> {
Output execute(Input input);

View file

@ -0,0 +1,28 @@
package dev.euph.engine.core.ecs;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
public abstract class Component {
@Getter
protected Entity entity = null;
protected List<Class<? extends Component>> requiredComponents = new ArrayList<>();
public void OnReady() {
}
public void OnUpdate(float deltaTime) {
}
public void OnDestroy() {
}
protected boolean hasRequiredComponents(Entity entity) {
return requiredComponents
.stream()
.allMatch(requiredClass -> entity.getComponent(requiredClass) != null);
}
}

View file

@ -0,0 +1,87 @@
package dev.euph.engine.core.ecs;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
public class Entity {
@Getter
private final String name;
@Getter
private final List<Component> components;
private Scene scene;
public Entity(String name) {
this.name = name;
components = new ArrayList<>();
}
public <T extends Component> Entity addComponent(T component) {
if (!component.hasRequiredComponents(this)) {
throw new IllegalArgumentException("Cannot add Component. Missing required Components.");
}
this.components.add(component);
if (this.scene != null) {
this.scene.updateEntityComponents(this);
}
if (component.entity != null) {
component.entity.removeComponent(component.getClass());
}
component.entity = this;
return this;
}
public <T extends Component> T getComponent(Class<T> componentClass) {
for (Component c : components) {
if (componentClass.isAssignableFrom(c.getClass())) {
try {
return componentClass.cast(c);
} catch (ClassCastException e) {
e.printStackTrace();
assert false : "Error: Casting component.";
}
}
}
return null;
}
public <T extends Component> void removeComponent(Class<T> componentClass) {
for (int i = 0; i < components.size(); i++) {
if (componentClass.isAssignableFrom(components.get(i).getClass())) {
components.remove(i);
if (this.scene != null) {
this.scene.updateEntityComponents(this);
}
return;
}
}
}
public void ready() {
for (Component component : components) {
component.OnReady();
}
}
public void update(float deltaTime) {
for (Component component : components) {
component.OnUpdate(deltaTime);
}
}
public void destroy() {
scene.removeEntity(this);
components.forEach(Component::OnDestroy);
}
public Entity addToScene(Scene scene) {
scene.addEntity(this);
this.scene = scene;
return this;
}
}

View file

@ -0,0 +1,12 @@
package dev.euph.engine.core.ecs;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Requires {
Class<? extends Component>[] value();
}

View file

@ -0,0 +1,101 @@
package dev.euph.engine.core.ecs;
import dev.euph.engine.core.data.Archetype;
import dev.euph.engine.core.ecs.storages.ComponentClassesBySuperclassStorage;
import dev.euph.engine.core.ecs.storages.ComponentsByTypeStorage;
import dev.euph.engine.core.ecs.storages.EntitiesByArchetypeStorage;
import dev.euph.engine.core.ecs.storages.EntitiesByNameStorage;
import lombok.Getter;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
public class Scene {
@Getter
private final Set<Entity> entities = new HashSet<>();
private final EntitiesByNameStorage entitiesByName = new EntitiesByNameStorage();
private final EntitiesByArchetypeStorage entitiesByArchetype = new EntitiesByArchetypeStorage();
private final ComponentClassesBySuperclassStorage componentClassesBySuperclass = new ComponentClassesBySuperclassStorage();
private final ComponentsByTypeStorage componentsByType = new ComponentsByTypeStorage();
@Getter
private boolean isRunning = false;
public void start() {
if (isRunning) return;
isRunning = true;
for (Entity entity : entities) {
entity.ready();
}
}
public void update(float deltaTime) {
entities.forEach(entity -> entity.update(deltaTime));
}
protected void removeEntity(Entity entity) {
entities.remove(entity);
entitiesByName.remove(entity);
entitiesByArchetype.remove(entity);
componentClassesBySuperclass.remove(entity);
componentsByType.remove(entity);
}
protected void updateEntityComponents(Entity entity) {
Set<Class<? extends Component>> components = entity.getComponents()
.stream()
.map(Component::getClass)
.collect(Collectors.toSet());
entitiesByName.update(entity, components);
entitiesByArchetype.update(entity, components);
componentClassesBySuperclass.update(entity, components);
componentsByType.update(entity, components);
}
protected void addEntity(Entity entity) {
if (entities.contains(entity)) {
return;
}
entities.add(entity);
Set<Class<? extends Component>> components = entity.getComponents()
.stream()
.map(Component::getClass)
.collect(Collectors.toSet());
entitiesByName.add(entity, components);
entitiesByArchetype.add(entity, components);
componentClassesBySuperclass.add(entity, components);
componentsByType.add(entity, components);
if (!isRunning) {
return;
}
entity.ready();
}
@SafeVarargs
public final Set<Entity> getByComponents(boolean exactMatch, Class<? extends Component>... componentClasses) {
Set<Entity> filteredEntities = new HashSet<>();
if (exactMatch) {
filteredEntities.addAll(entitiesByArchetype.get(new Archetype<>(componentClasses)));
} else {
Set<Class<? extends Component>> componentSet = componentClassesBySuperclass.get(Set.of(componentClasses));
Set<Component> componentInstances = componentsByType.get(componentSet);
if (componentInstances != null) {
filteredEntities.addAll(componentInstances.stream()
.map(Component::getEntity)
.toList());
}
}
return filteredEntities;
}
public final Set<Entity> getByName(String name) {
return entitiesByName.get(name);
}
}

View file

@ -0,0 +1,15 @@
package dev.euph.engine.core.ecs;
import java.util.Set;
public interface SceneStorage<T, U> {
void add(Entity entity, Set<Class<? extends Component>> componentClasses);
void remove(Entity entity);
void remove(Set<Entity> entities);
void update(Entity entity, Set<Class<? extends Component>> newComponentClasses);
Set<U> get(T identifier);
}

View file

@ -0,0 +1,59 @@
package dev.euph.engine.core.ecs.storages;
import dev.euph.engine.core.ecs.Component;
import dev.euph.engine.core.ecs.Entity;
import dev.euph.engine.core.ecs.SceneStorage;
import java.util.*;
public class ComponentClassesBySuperclassStorage implements SceneStorage<Set<Class<? extends Component>>, Class<? extends Component>> {
private final Map<Class<? extends Component>, Set<Class<? extends Component>>> subclassesByComponent;
public ComponentClassesBySuperclassStorage() {
this.subclassesByComponent = new HashMap<>();
}
@Override
public void add(Entity entity, Set<Class<? extends Component>> componentClasses) {
for (Class<? extends Component> componentClass : componentClasses) {
Set<Class<? extends Component>> subclasses = getSubclasses(componentClass);
subclassesByComponent.put(componentClass, subclasses);
}
}
@Override
public void remove(Entity entity) {
}
@Override
public void remove(Set<Entity> entities) {
}
@Override
public void update(Entity entity, Set<Class<? extends Component>> newComponentClasses) {
remove(entity);
}
@Override
public Set<Class<? extends Component>> get(Set<Class<? extends Component>> identifier) {
Set<Class<? extends Component>> componentSet = new HashSet<>();
for (Class<? extends Component> componentClass : identifier) {
componentSet.add(componentClass);
componentSet.addAll(subclassesByComponent.getOrDefault(componentClass, Collections.emptySet()));
}
return componentSet;
}
private Set<Class<? extends Component>> getSubclasses(Class<? extends Component> componentClass) {
Set<Class<? extends Component>> subclasses = new HashSet<>();
for (Class<? extends Component> clazz : subclassesByComponent.keySet()) {
if (componentClass.isAssignableFrom(clazz)) {
subclasses.add(clazz);
}
}
return subclasses;
}
}

View file

@ -0,0 +1,71 @@
package dev.euph.engine.core.ecs.storages;
import dev.euph.engine.core.ecs.Component;
import dev.euph.engine.core.ecs.Entity;
import dev.euph.engine.core.ecs.SceneStorage;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class ComponentsByTypeStorage implements SceneStorage<Set<Class<? extends Component>>, Component> {
private final Map<Class<? extends Component>, Set<Component>> componentInstancesByType;
public ComponentsByTypeStorage() {
this.componentInstancesByType = new HashMap<>();
}
@Override
public void add(Entity entity, Set<Class<? extends Component>> componentClasses) {
for (Class<? extends Component> componentClass : componentClasses) {
Set<Component> componentInstances = componentInstancesByType.computeIfAbsent(componentClass, key -> new HashSet<>());
componentInstances.add(entity.getComponent(componentClass)); // added
Class<?> superClass = componentClass.getSuperclass();
while (Component.class.isAssignableFrom(superClass)) {
if (Component.class.isAssignableFrom(superClass)) {
Set<Component> superClassInstances = componentInstancesByType.computeIfAbsent(superClass.asSubclass(Component.class), key -> new HashSet<>());
superClassInstances.add(entity.getComponent(componentClass));
}
superClass = superClass.getSuperclass();
}
}
}
@Override
public void remove(Entity entity) {
for (Component component : entity.getComponents()) {
Class<? extends Component> componentClass = component.getClass();
Set<Component> componentInstances = componentInstancesByType.get(componentClass);
if (componentInstances != null) {
componentInstances.remove(component);
}
}
}
@Override
public void remove(Set<Entity> entities) {
}
@Override
public void update(Entity entity, Set<Class<? extends Component>> newComponentClasses) {
for (Component component : entity.getComponents()) {
Class<? extends Component> componentClass = component.getClass();
Set<Component> componentInstances = componentInstancesByType.computeIfAbsent(componentClass, key -> new HashSet<>());
componentInstances.add(component);
}
}
@Override
public Set<Component> get(Set<Class<? extends Component>> identifier) {
Set<Component> filteredEntities = new HashSet<>();
for (Class<? extends Component> componentClass : identifier) {
filteredEntities.addAll(componentInstancesByType.get(componentClass));
}
return filteredEntities;
}
}

View file

@ -0,0 +1,56 @@
package dev.euph.engine.core.ecs.storages;
import dev.euph.engine.core.data.Archetype;
import dev.euph.engine.core.ecs.Component;
import dev.euph.engine.core.ecs.Entity;
import dev.euph.engine.core.ecs.SceneStorage;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class EntitiesByArchetypeStorage implements SceneStorage<Archetype<Component>, Entity> {
private final Map<Archetype<Component>, Set<Entity>> entitiesByArchetype;
public EntitiesByArchetypeStorage() {
this.entitiesByArchetype = new HashMap<>();
}
@Override
public void add(Entity entity, Set<Class<? extends Component>> componentClasses) {
Archetype<Component> type = new Archetype<>(componentClasses);
Set<Entity> entitySet = entitiesByArchetype.computeIfAbsent(type, _ -> new HashSet<>());
entitySet.add(entity);
this.entitiesByArchetype.put(type, entitySet);
}
@Override
public void remove(Entity entity) {
for (Map.Entry<Archetype<Component>, Set<Entity>> entry : entitiesByArchetype.entrySet()) {
Archetype<Component> archetype = entry.getKey();
Set<Entity> archetypeEntities = entry.getValue();
if (!archetypeEntities.remove(entity)) {
return;
}
if (archetypeEntities.isEmpty()) {
entitiesByArchetype.remove(archetype);
}
}
}
@Override
public void remove(Set<Entity> entities) {
entities.forEach(this::remove);
}
@Override
public void update(Entity entity, Set<Class<? extends Component>> newComponentClasses) {
}
@Override
public Set<Entity> get(Archetype<Component> identifier) {
return this.entitiesByArchetype.get(identifier);
}
}

View file

@ -0,0 +1,58 @@
package dev.euph.engine.core.ecs.storages;
import dev.euph.engine.core.ecs.Component;
import dev.euph.engine.core.ecs.Entity;
import dev.euph.engine.core.ecs.SceneStorage;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class EntitiesByNameStorage implements SceneStorage<String, Entity> {
private final Map<String, Set<Entity>> entitiesByName;
public EntitiesByNameStorage() {
this.entitiesByName = new HashMap<>();
}
@Override
public void add(Entity entity, Set<Class<? extends Component>> componentClasses) {
Set<Entity> entities = this.entitiesByName.computeIfAbsent(entity.getName(), _ -> new HashSet<>());
if (entities == null) {
entities = new HashSet<>();
}
entities.add(entity);
this.entitiesByName.put(entity.getName(), entities);
}
@Override
public void remove(Entity entity) {
Set<Entity> entities = this.entitiesByName.get(entity.getName());
if (entities == null) {
return;
}
if (entities.size() == 1 && entities.contains(entity)) {
this.entitiesByName.remove(entity.getName());
return;
}
entities.remove(entity);
this.entitiesByName.put(entity.getName(), entities);
}
@Override
public void remove(Set<Entity> entities) {
entities.forEach(this::remove);
}
@Override
public void update(Entity entity, Set<Class<? extends Component>> newComponentClasses) {
}
@Override
public Set<Entity> get(String identifier) {
return entitiesByName.get(identifier);
}
}

View file

@ -1,73 +1,84 @@
package dev.euph.engine.ecs.components;
package dev.euph.engine.core.transform;
import dev.euph.engine.ecs.Component;
import dev.euph.engine.math.TransformationMatrix;
import org.joml.Matrix4f;
import dev.euph.engine.core.ecs.Component;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import javax.annotation.PostConstruct;
import static java.lang.Math.toRadians;
@Getter
@NoArgsConstructor
public class Transform extends Component {
//region Fields
private Vector3f position;
private Vector3f rotation;
private Vector3f scale;
private TransformationMatrix transformationMatrix;
//endregion
//region Constructor
public Transform() {
this(new Vector3f(), new Vector3f(), new Vector3f(1));
}
private Vector3f position = new Vector3f();
private Vector3f rotation = new Vector3f();
private Vector3f scale = new Vector3f(1);
private TransformationMatrix transformationMatrix;
public Transform(Vector3f position, Vector3f rotation, Vector3f scale) {
this.position = position;
this.rotation = rotation;
this.scale = scale;
calculateTransformationMatrix();
}
public Transform(Transform transform) {
this.position = transform.position;
this.rotation = transform.rotation;
this.scale = transform.scale;
calculateTransformationMatrix();
}
//endregion
//region Utility
public static Transform FromPosition(float x, float y, float z) {
return Transform.FromPosition(new Vector3f(x, y, z));
}
public static Transform FromPosition(Vector3f position) {
return new Transform(position, new Vector3f(), new Vector3f(1));
}
public static Transform FromRotation(float x, float y, float z) {
return Transform.FromRotation(new Vector3f(x, y, z));
}
public static Transform FromRotation(Vector3f rotation) {
return new Transform(new Vector3f(), rotation, new Vector3f(1));
}
public static Transform FromScale(float x, float y, float z) {
return Transform.FromScale(new Vector3f(x, y, z));
}
public static Transform FromScale(Vector3f scale) {
return new Transform(new Vector3f(), new Vector3f(), scale);
}
@PostConstruct
private void calculateTransformationMatrix() {
transformationMatrix = new TransformationMatrix(this);
}
//endregion
//region Getter/Setter
public Vector3f getPosition() {
return new Vector3f(position);
}
public void setPosition(Vector3f position) {
this.position = position;
calculateTransformationMatrix();
}
public Vector3f getRotation() {
return new Vector3f(rotation);
}
public void setRotation(Vector3f rotation) {
this.rotation = rotation;
calculateTransformationMatrix();
}
public Vector3f getScale() {
return new Vector3f(scale);
public Transform setScale(float scale) {
return setScale(new Vector3f(scale));
}
public void setScale(Vector3f scale) {
public Transform setScale(Vector3f scale) {
this.scale = scale;
calculateTransformationMatrix();
}
public TransformationMatrix getTransformationMatrix() {
return transformationMatrix;
return this;
}
public Vector3f getUp() {
@ -104,5 +115,4 @@ public class Transform extends Component {
);
return q.positiveZ(new Vector3f());
}
//endregion
}

View file

@ -1,6 +1,5 @@
package dev.euph.engine.math;
package dev.euph.engine.core.transform;
import dev.euph.engine.ecs.components.Transform;
import org.joml.Matrix4f;
import org.joml.Vector3f;
@ -15,6 +14,7 @@ public class TransformationMatrix extends Matrix4f {
super.rotate((float) Math.toRadians(rotation.z % 360), new Vector3f(0, 0, 1));
super.scale(scale);
}
public TransformationMatrix(Transform transform) {
super();
super.identity();
@ -25,6 +25,7 @@ public class TransformationMatrix extends Matrix4f {
super.rotate((float) Math.toRadians(rotation.z % 360), new Vector3f(0, 0, 1));
super.scale(transform.getScale());
}
public TransformationMatrix() {
super();
super.identity();

View file

@ -0,0 +1,24 @@
package dev.euph.engine.core.util;
import lombok.Getter;
import org.lwjgl.glfw.GLFW;
public class Time {
private long lastFrameTime;
@Getter
private float deltaTime;
public void startDeltaTime() {
lastFrameTime = getCurrentTime();
}
public void updateDeltaTime() {
long currentFrameTime = getCurrentTime();
deltaTime = (currentFrameTime - lastFrameTime) / 1000f;
lastFrameTime = currentFrameTime;
}
private long getCurrentTime() {
return (long) (GLFW.glfwGetTime() * 1000L);
}
}

View file

@ -0,0 +1,5 @@
package dev.euph.engine.core.window;
public interface CloseCallback {
void onClose();
}

View file

@ -0,0 +1,5 @@
package dev.euph.engine.core.window;
public interface ResizeCallback {
void onResize(int width, int height);
}

View file

@ -0,0 +1,127 @@
package dev.euph.engine.core.window;
import dev.euph.engine.core.util.Time;
import lombok.Getter;
import org.lwjgl.glfw.GLFWErrorCallback;
import org.lwjgl.glfw.GLFWWindowCloseCallback;
import org.lwjgl.glfw.GLFWWindowSizeCallback;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import static org.lwjgl.glfw.Callbacks.glfwFreeCallbacks;
import static org.lwjgl.glfw.GLFW.*;
@Getter
public abstract class Window {
//@Getter(AccessLevel.NONE)
protected final long id;
protected final Time time;
protected final int initialWidth, initialHeight;
private final Set<CloseCallback> closeCallbacks = new HashSet<>();
private final Set<ResizeCallback> resizeCallbacks = new HashSet<>();
protected int width, height;
public Window(int width, int height, String title) {
this(width, height, title, 0, new Time());
}
public Window(int width, int height, String title, int vsyncInterval) {
this(width, height, title, vsyncInterval, new Time());
}
private Window(int width, int height, String title, int vsyncInterval, Time time) {
this.time = time;
this.initialWidth = width;
this.width = width;
this.initialHeight = height;
this.height = height;
GLFWErrorCallback.createPrint(System.err).set();
if (!glfwInit()) {
throw new IllegalStateException("Unable to initialize GLFW");
}
glfwDefaultWindowHints();
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
this.id = glfwCreateWindow(width, height, title, 0, 0);
if (id == 0) {
throw new RuntimeException("Failed to create the GLFW window");
}
setupCallbacks();
glfwMakeContextCurrent(id);
glfwSwapInterval(vsyncInterval);
glfwShowWindow(id);
time.startDeltaTime();
}
private void setupCallbacks() {
GLFWWindowCloseCallback closeCallback = new GLFWWindowCloseCallback() {
@Override
public void invoke(long window) {
closeCallbacks.forEach(CloseCallback::onClose);
}
};
glfwSetWindowCloseCallback(id, closeCallback);
GLFWWindowSizeCallback windowSizeCallback = new GLFWWindowSizeCallback() {
@Override
public void invoke(long window, int newWidth, int newHeight) {
Window.this.width = newWidth;
Window.this.height = newHeight;
resizeCallbacks.forEach(callback -> callback.onResize(newWidth, newHeight));
}
};
glfwSetWindowSizeCallback(id, windowSizeCallback);
}
public void update() {
glfwSwapBuffers(id);
glfwPollEvents();
time.updateDeltaTime();
onUpdate();
}
public void destroy() {
glfwFreeCallbacks(id);
glfwDestroyWindow(id);
glfwTerminate();
Objects.requireNonNull(glfwSetErrorCallback(null)).free();
onDestroy();
}
public void resize(int width, int height) {
glfwSetWindowSize(id, width, height);
resizeCallbacks.forEach(callback -> callback.onResize(width, height));
}
public void setVsync(int interval) {
glfwSwapInterval(interval);
}
public void addResizeCallback(ResizeCallback resizeCallback) {
resizeCallbacks.add(resizeCallback);
}
public void removeResizeCallback(ResizeCallback resizeCallback) {
resizeCallbacks.remove(resizeCallback);
}
public void addCloseCallback(CloseCallback closeCallback) {
closeCallbacks.add(closeCallback);
}
public void removeCloseCallback(CloseCallback closeCallback) {
closeCallbacks.remove(closeCallback);
}
protected abstract void onUpdate();
protected abstract void onDestroy();
}

View file

@ -0,0 +1,49 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
repositories {
mavenCentral()
}
plugins {
java
id("com.gradleup.shadow") version "8.3.0"
}
val lwjglVersion: String by rootProject.extra
val jomlVersion: String by rootProject.extra
val jomlPrimitivesVersion: String by rootProject.extra
val lwjglNatives: String by rootProject.extra
sourceSets {
val main by getting {
java.srcDir("java")
}
}
dependencies {
annotationProcessor("org.projectlombok:lombok:1.18.34")
implementation("org.projectlombok:lombok:1.18.34")
annotationProcessor("javax.annotation:javax.annotation-api:1.3.2")
implementation("javax.annotation:javax.annotation-api:1.3.2")
compileOnly(project(":core", configuration = "shadow"))
compileOnly(project(":rendering", configuration = "shadow"))
implementation("org.lwjgl", "lwjgl-assimp")
runtimeOnly("org.lwjgl", "lwjgl-assimp", classifier = lwjglNatives)
implementation(platform("org.lwjgl:lwjgl-bom:$lwjglVersion"))
implementation("org.lwjgl", "lwjgl-opengl")
runtimeOnly("org.lwjgl", "lwjgl-opengl", classifier = lwjglNatives)
shadow(localGroovy())
shadow(gradleApi())
}
tasks.named<ShadowJar>("shadowJar") {
archiveFileName.set("dev.euph.engine.opengl.jar")
}
configurations {
shadow
}

View file

@ -0,0 +1,44 @@
package dev.euph.engine.opengl;
import org.lwjgl.opengl.GL31;
public class GL extends GL31 {
protected static int getTextureIndex(int index) {
return switch (index) {
case 0 -> GL_TEXTURE0;
case 1 -> GL_TEXTURE1;
case 2 -> GL_TEXTURE2;
case 3 -> GL_TEXTURE3;
case 4 -> GL_TEXTURE4;
case 5 -> GL_TEXTURE5;
case 6 -> GL_TEXTURE6;
case 7 -> GL_TEXTURE7;
case 8 -> GL_TEXTURE8;
case 9 -> GL_TEXTURE9;
case 10 -> GL_TEXTURE10;
case 11 -> GL_TEXTURE11;
case 12 -> GL_TEXTURE12;
case 13 -> GL_TEXTURE13;
case 14 -> GL_TEXTURE14;
case 15 -> GL_TEXTURE15;
case 16 -> GL_TEXTURE16;
case 17 -> GL_TEXTURE17;
case 18 -> GL_TEXTURE18;
case 19 -> GL_TEXTURE19;
case 20 -> GL_TEXTURE20;
case 21 -> GL_TEXTURE21;
case 22 -> GL_TEXTURE22;
case 23 -> GL_TEXTURE23;
case 24 -> GL_TEXTURE24;
case 25 -> GL_TEXTURE25;
case 26 -> GL_TEXTURE26;
case 27 -> GL_TEXTURE27;
case 28 -> GL_TEXTURE28;
case 29 -> GL_TEXTURE29;
case 30 -> GL_TEXTURE30;
case 31 -> GL_TEXTURE31;
default -> throw new IndexOutOfBoundsException("There are Only 32 GL Textures");
};
}
}

View file

@ -0,0 +1,34 @@
package dev.euph.engine.opengl;
import dev.euph.engine.rendering.resources.Mesh;
import static org.lwjgl.opengl.GL30.glBindVertexArray;
public class GLMesh implements Mesh {
private final int vaoId;
private final int vertexCount;
public GLMesh(float[] vertices, int[] indices) {
this.vertexCount = indices.length;
vaoId = GLMeshHelper.getVao();
GLMeshHelper.prepareLoad(vaoId);
GLMeshHelper.loadVertices(vertices);
GLMeshHelper.loadIndices(indices);
GLMeshHelper.cleanupLoad();
}
@Override
public void bind() {
glBindVertexArray(vaoId);
}
@Override
public void unbind() {
glBindVertexArray(0);
}
@Override
public int getVertexCount() {
return vertexCount;
}
}

View file

@ -0,0 +1,63 @@
package dev.euph.engine.opengl;
import org.lwjgl.BufferUtils;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import static org.lwjgl.opengl.GL11.GL_FLOAT;
import static org.lwjgl.opengl.GL15.*;
import static org.lwjgl.opengl.GL20.glVertexAttribPointer;
import static org.lwjgl.opengl.GL30.glBindVertexArray;
import static org.lwjgl.opengl.GL30.glGenVertexArrays;
public class GLMeshHelper {
private final static int VERTICE_INDEX = 0;
private final static int TEXTURE_COORDINATE_INDEX = 1;
protected static int getVao() {
return glGenVertexArrays();
}
protected static void prepareLoad(int vaoId) {
glBindVertexArray(vaoId);
}
protected static void cleanupLoad() {
glBindVertexArray(0);
}
protected static void loadVertices(float[] vertices) {
int vboId = glGenBuffers();
glBindBuffer(GL_ARRAY_BUFFER, vboId);
FloatBuffer buffer = BufferUtils.createFloatBuffer(vertices.length);
buffer.put(vertices).flip();
glBufferData(GL_ARRAY_BUFFER, buffer, GL_STATIC_DRAW);
glVertexAttribPointer(VERTICE_INDEX, 3, GL_FLOAT, false, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
protected static void loadIndices(int[] indices) {
int eboId = glGenBuffers();
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboId);
IntBuffer buffer = BufferUtils.createIntBuffer(indices.length);
buffer.put(indices).flip();
glBufferData(GL_ELEMENT_ARRAY_BUFFER, buffer, GL_STATIC_DRAW);
}
protected static void loadTextureCoordinates(float[] textureCoordinates) {
int vboTextureCoords = glGenBuffers();
glBindBuffer(GL_ARRAY_BUFFER, vboTextureCoords);
FloatBuffer buffer = BufferUtils.createFloatBuffer(textureCoordinates.length);
buffer.put(textureCoordinates).flip();
glBufferData(GL_ARRAY_BUFFER, buffer, GL_STATIC_DRAW);
glVertexAttribPointer(TEXTURE_COORDINATE_INDEX, 2, GL_FLOAT, false, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
}

View file

@ -0,0 +1,108 @@
package dev.euph.engine.opengl;
import dev.euph.engine.rendering.resources.Mesh;
import org.jetbrains.annotations.NotNull;
import org.lwjgl.assimp.*;
import java.net.URL;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class GLMeshLoader{
public static Mesh loadMesh(URL modelResource) {
try {
AIMesh mesh = loadMeshIntoAssimp(modelResource);
List<Float> verticesList = new ArrayList<>();
for (int i = 0; i < mesh.mNumVertices(); i++) {
loadVertex(mesh, i, verticesList);
}
float[] vertices = listToFloatArray(verticesList);
int[] indices = listToIntArray(loadIndices(mesh));
return new GLMesh(vertices, indices);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Error loading mesh: " + modelResource.getPath());
}
}
public static Mesh loadTexturedMesh(URL modelResource) {
try {
AIMesh mesh = loadMeshIntoAssimp(modelResource);
List<Float> verticesList = new ArrayList<>();
List<Float> textureCoordinatesList = new ArrayList<>();
for (int i = 0; i < mesh.mNumVertices(); i++) {
loadVertex(mesh, i, verticesList);
loadTextureCoordinates(mesh, i, textureCoordinatesList);
}
float[] vertices = listToFloatArray(verticesList);
int[] indices = listToIntArray(loadIndices(mesh));
float[] texCoords = listToFloatArray(textureCoordinatesList);
return new GLTexturedMesh(vertices, indices, texCoords);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Error loading mesh: " + modelResource.getPath());
}
}
private static void loadTextureCoordinates(AIMesh mesh, int i, List<Float> texCoordsList) {
AIVector3D textureCoordinate = mesh.mTextureCoords(0).get(i);
texCoordsList.add(textureCoordinate.x());
texCoordsList.add(textureCoordinate.y());
}
private static void loadVertex(AIMesh mesh, int i, List<Float> verticesList) {
AIVector3D vertex = mesh.mVertices().get(i);
verticesList.add(vertex.x());
verticesList.add(vertex.y());
verticesList.add(vertex.z());
}
private static List<Integer> loadIndices(AIMesh mesh) {
List<Integer> indicesList = new ArrayList<>();
AIFace.Buffer faceBuffer = mesh.mFaces();
while (faceBuffer.remaining() > 0) {
AIFace face = faceBuffer.get();
IntBuffer buffer = face.mIndices();
while (buffer.remaining() > 0) {
indicesList.add(buffer.get());
}
}
return indicesList;
}
private static @NotNull AIMesh loadMeshIntoAssimp(URL modelResource) {
AIScene scene = Assimp.aiImportFile(modelResource.getPath(), Assimp.aiProcess_Triangulate | Assimp.aiProcess_FlipUVs);
if (scene == null || scene.mNumMeshes() == 0) {
throw new RuntimeException("Failed to load mesh: " + modelResource.getPath());
}
AIMesh mesh = AIMesh.create(Objects.requireNonNull(scene.mMeshes()).get(0));
scene.close();
return mesh;
}
private static float[] listToFloatArray(List<Float> list) {
float[] array = new float[list.size()];
for (int i = 0; i < list.size(); i++) {
array[i] = list.get(i);
}
return array;
}
private static int[] listToIntArray(List<Integer> list) {
int[] array = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
array[i] = list.get(i);
}
return array;
}
}

View file

@ -0,0 +1,90 @@
package dev.euph.engine.opengl;
import dev.euph.engine.core.transform.Transform;
import dev.euph.engine.core.ecs.Entity;
import dev.euph.engine.core.ecs.Scene;
import dev.euph.engine.core.transform.TransformationMatrix;
import dev.euph.engine.rendering.Material;
import dev.euph.engine.rendering.RenderPipeline;
import dev.euph.engine.rendering.Shader;
import dev.euph.engine.rendering.ShaderManager;
import dev.euph.engine.rendering.components.Camera;
import dev.euph.engine.rendering.components.MeshRenderer;
import dev.euph.engine.rendering.resources.Mesh;
import dev.euph.engine.rendering.utils.ProjectionMatrix;
import dev.euph.engine.rendering.utils.ViewMatrix;
import lombok.Getter;
import lombok.Setter;
import static dev.euph.engine.opengl.GL.*;
public class GLRenderer extends RenderPipeline {
private final ShaderManager shaderManager;
public Camera activeCamera;
@Getter
@Setter
private Scene scene;
public GLRenderer(Scene scene, ShaderManager shaderManager) {
this.scene = scene;
this.shaderManager = shaderManager;
}
@Override
public void init() {
glEnable(GL_DEPTH_TEST);
}
@Override
public void render() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
if (activeCamera == null) {
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
return;
}
glClearColor(
activeCamera.backgroundColor.getRed(),
activeCamera.backgroundColor.getGreen(),
activeCamera.backgroundColor.getBlue(),
activeCamera.backgroundColor.getAlpha()
);
Shader shader = shaderManager.getShader("default");
if (shader == null) return;
startShader(shader);
ViewMatrix viewMatrix = new ViewMatrix(activeCamera);
loadMatrix4(shader, getUniformLocation(shader, "viewMatrix"), viewMatrix);
ProjectionMatrix projectionMatrix = new ProjectionMatrix(activeCamera);
loadMatrix4(shader, getUniformLocation(shader, "projectionMatrix"), projectionMatrix);
stopShader(shader);
for (Entity entity : scene.getByComponents(false, MeshRenderer.class)) {
renderEntity(entity, entity.getComponent(MeshRenderer.class));
}
}
private void renderEntity(Entity entity, MeshRenderer meshRenderer) {
Mesh mesh = meshRenderer.getMesh();
Material material = meshRenderer.getMaterial();
Shader shader = material.getShader();
if (shader == null || mesh == null) return;
startShader(shader);
bindShaderAttributes(shader);
TransformationMatrix transformationMatrix = entity.getComponent(Transform.class).getTransformationMatrix();
loadMatrix4(shader, getUniformLocation(shader, "transformationMatrix"), transformationMatrix);
material.useMaterial();
mesh.bind();
enableShaderAttributes(shader);
glDrawElements(GL_TRIANGLES, mesh.getVertexCount(), GL_UNSIGNED_INT, 0);
disableShaderAttributes(shader);
mesh.unbind();
stopShader(shader);
}
}

View file

@ -1,5 +1,7 @@
package dev.euph.engine.render;
package dev.euph.engine.opengl;
import dev.euph.engine.rendering.Shader;
import dev.euph.engine.rendering.resources.Texture;
import org.joml.Matrix4f;
import org.joml.Vector2f;
import org.joml.Vector3f;
@ -7,138 +9,45 @@ import org.joml.Vector4f;
import org.lwjgl.BufferUtils;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.FloatBuffer;
import java.util.HashMap;
import java.util.Map;
import static org.lwjgl.opengl.GL40.*;
import static dev.euph.engine.opengl.GL.*;
public class Shader implements AutoCloseable {
//region Fields
private final String name;
private final int programId;
private final int vertexShaderId;
private final int fragmentShaderId;
public class GLShader extends Shader {
private static final FloatBuffer matrixBuffer = BufferUtils.createFloatBuffer(16);
private final int programId;
private final Map<String, Integer> uniformLocations = new HashMap<>();
//endregion
//region Constructors
public Shader(String name, String vertexFile, String fragmentFile) throws IOException {
this.name = name;
vertexShaderId = loadShader(vertexFile, GL_VERTEX_SHADER);
fragmentShaderId = loadShader(fragmentFile, GL_FRAGMENT_SHADER);
public GLShader(String name, URL vertexShader, URL fragmentShader) throws IOException {
super(
name,
loadShader(vertexShader.openStream(), GL_VERTEX_SHADER),
loadShader(fragmentShader.openStream(), GL_FRAGMENT_SHADER)
);
programId = glCreateProgram();
glAttachShader(programId, vertexShaderId);
glAttachShader(programId, fragmentShaderId);
bindAttributes();
glLinkProgram(programId);
glValidateProgram(programId);
getAllUniformLocations();
}
//endregion
//region Public Methods
public void start() {
glUseProgram(programId);
setupProgram();
}
public void stop() {
glUseProgram(0);
public GLShader(String name, InputStream vertexShaderStream, InputStream fragmentShaderStream) throws IOException {
super(
name,
loadShader(vertexShaderStream, GL_VERTEX_SHADER),
loadShader(fragmentShaderStream, GL_FRAGMENT_SHADER)
);
programId = glCreateProgram();
setupProgram();
}
@Override
public void close() {
stop();
glDetachShader(programId, vertexShaderId);
glDetachShader(programId, fragmentShaderId);
glDeleteShader(vertexShaderId);
glDeleteShader(fragmentShaderId);
glDeleteProgram(programId);
}
//endregion
//region Uniform Locations
protected void getAllUniformLocations() {
int numUniforms = glGetProgrami(programId, GL_ACTIVE_UNIFORMS);
for (int i = 0; i < numUniforms; i++) {
String name = glGetActiveUniformName(programId, i);
int location = glGetUniformLocation(programId, name);
uniformLocations.put(name, location);
}
}
public int getUniformLocation(String uniformName) {
if (uniformLocations.containsKey(uniformName)) {
return uniformLocations.get(uniformName);
}
int location = glGetUniformLocation(programId, uniformName);
uniformLocations.put(uniformName, location);
return location;
}
//endregion
//region Attributes
public void enableAttributes(){
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);
}
public void disableAttributes(){
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glDisableVertexAttribArray(2);
}
public void bindAttributes() {
bindAttribute(0, "vertexPositions");
bindAttribute(1, "textureCoords");
bindAttribute(2, "normals");
}
protected void bindAttribute(int attribute, String variableName) {
glBindAttribLocation(programId, attribute, variableName);
}
//endregion
//region Uniform Loaders
public void loadBoolean(int location, boolean value) {
glUniform1f(location, value ? 1f : 0f);
}
public void loadInt(int location, int value) {
glUniform1i(location, value);
}
public void loadFloat(int location, float value) {
glUniform1f(location, value);
}
public void loadVector2(int location, Vector2f value) {
glUniform2f(location, value.x, value.y);
}
public void loadVector3(int location, Vector3f value) {
glUniform3f(location, value.x, value.y, value.z);
}
public void loadVector4(int location, Vector4f value) {
glUniform4f(location, value.x, value.y, value.z, value.w);
}
public void loadMatrix4(int location, Matrix4f value) {
value.get(matrixBuffer);
glUniformMatrix4fv(location, false, matrixBuffer);
}
//endregion
//region Helper Methods
private static int loadShader(String file, int type) throws IOException{
private static int loadShader(InputStream shaderStream, int type) throws IOException {
StringBuilder shaderSource = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(shaderStream))) {
String line;
while ((line = reader.readLine()) != null) {
shaderSource.append(line).append("\n");
@ -158,10 +67,134 @@ public class Shader implements AutoCloseable {
}
return shaderId;
}
//endregion
private void setupProgram() {
glAttachShader(programId, vertexShaderId);
glAttachShader(programId, fragmentShaderId);
bindAttributes();
glLinkProgram(programId);
glValidateProgram(programId);
getAllUniformLocations();
}
public String getName() {
return name;
@Override
public void start() {
glUseProgram(programId);
}
@Override
public void stop() {
glUseProgram(0);
}
@Override
public void close() {
stop();
glDetachShader(programId, vertexShaderId);
glDetachShader(programId, fragmentShaderId);
glDeleteShader(vertexShaderId);
glDeleteShader(fragmentShaderId);
glDeleteProgram(programId);
}
private void getAllUniformLocations() {
int numUniforms = glGetProgrami(programId, GL_ACTIVE_UNIFORMS);
for (int i = 0; i < numUniforms; i++) {
String name = glGetActiveUniformName(programId, i);
int location = glGetUniformLocation(programId, name);
uniformLocations.put(name, location);
}
}
@Override
protected int getUniformLocation(String uniformName) {
if (uniformLocations.containsKey(uniformName)) {
return uniformLocations.get(uniformName);
}
int location = glGetUniformLocation(programId, uniformName);
uniformLocations.put(uniformName, location);
return location;
}
@Override
protected void bindAttributes() {
glBindAttribLocation(programId, 0, "vertexPositions");
glBindAttribLocation(programId, 1, "textureCoords");
glBindAttribLocation(programId, 2, "normals");
}
@Override
protected void enableAttributes() {
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);
}
@Override
protected void disableAttributes() {
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glDisableVertexAttribArray(2);
}
@Override
protected void loadBoolean(int location, boolean value) {
glUniform1f(location, value ? 1f : 0f);
}
@Override
protected void loadInt(int location, int value) {
glUniform1i(location, value);
}
@Override
protected void loadFloat(int location, float value) {
glUniform1f(location, value);
}
@Override
protected void loadVector2(int location, Vector2f value) {
glUniform2f(location, value.x, value.y);
}
@Override
protected void loadVector3(int location, Vector3f value) {
glUniform3f(location, value.x, value.y, value.z);
}
@Override
protected void loadVector4(int location, Vector4f value) {
glUniform4f(location, value.x, value.y, value.z, value.w);
}
@Override
protected void loadMatrix4(int location, Matrix4f value) {
value.get(matrixBuffer);
glUniformMatrix4fv(location, false, matrixBuffer);
}
@Override
protected void deleteTexture(Texture texture) {
if(texture == null) {
return;
}
glDeleteTextures(texture.getId());
}
@Override
protected void bindTexture2D(int location, Texture texture, int textureIndex) {
loadInt(location, textureIndex);
glActiveTexture(getTextureIndex(textureIndex));
glBindTexture(GL_TEXTURE_2D, texture.getId());
}
@Override
protected void bindTexture3D(int location, Texture texture, int textureIndex) {
loadInt(location, textureIndex);
glActiveTexture(getTextureIndex(textureIndex));
glBindTexture(GL_TEXTURE_3D, texture.getId());
}
}

View file

@ -0,0 +1,35 @@
package dev.euph.engine.opengl;
import dev.euph.engine.core.data.ImageLoader;
import dev.euph.engine.rendering.resources.Texture;
import dev.euph.engine.rendering.resources.loader.TextureLoader;
import java.net.URL;
import static dev.euph.engine.opengl.GL.*;
public class GLTextureLoader {
public static Texture loadTexture(URL imageResource) {
int textureID = GL.glGenTextures();
glBindTexture(GL_TEXTURE_2D, textureID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
ImageLoader.Image image = ImageLoader.loadImage(imageResource.getPath());
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_RGBA,
image.width(), image.height(),
0,
GL_RGBA,
GL_UNSIGNED_BYTE,
image.data()
);
glGenerateMipmap(GL_TEXTURE_2D);
return new Texture(textureID, image.width(), image.height());
}
}

View file

@ -0,0 +1,35 @@
package dev.euph.engine.opengl;
import dev.euph.engine.rendering.resources.Mesh;
import static org.lwjgl.opengl.GL30.glBindVertexArray;
public class GLTexturedMesh implements Mesh {
private final int vaoId;
private final int vertexCount;
public GLTexturedMesh(float[] vertices, int[] indices, float[] textureCoords) {
this.vertexCount = indices.length;
vaoId = GLMeshHelper.getVao();
GLMeshHelper.prepareLoad(vaoId);
GLMeshHelper.loadVertices(vertices);
GLMeshHelper.loadIndices(indices);
GLMeshHelper.loadTextureCoordinates(textureCoords);
GLMeshHelper.cleanupLoad();
}
@Override
public void bind() {
glBindVertexArray(vaoId);
}
@Override
public void unbind() {
glBindVertexArray(0);
}
@Override
public int getVertexCount() {
return vertexCount;
}
}

View file

@ -0,0 +1,51 @@
package dev.euph.engine.opengl;
import dev.euph.engine.core.window.Window;
import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.opengl.GL;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL11.glViewport;
public class GLWindow extends Window {
public GLWindow(int width, int height, String title) {
super(width, height, title);
glfwSetKeyCallback(id, (window, key, scancode, action, mods) -> {
if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
glfwSetWindowShouldClose(window, true);
}
});
if (glfwGetPlatform() != GLFW_PLATFORM_WAYLAND) {
GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor());
assert vidmode != null;
glfwSetWindowPos(
id,
(vidmode.width() - width) / 2,
(vidmode.height() - height) / 2
);
}
addResizeCallback((int newWidth, int newHeight) -> {
GLWindow.this.width = newWidth;
GLWindow.this.height = newHeight;
glViewport(0, 0, newWidth, newHeight);
});
GL.createCapabilities();
}
@Override
protected void onUpdate() {
}
@Override
protected void onDestroy() {
}
}

View file

@ -0,0 +1,42 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
repositories {
mavenCentral()
}
plugins {
java
id("com.gradleup.shadow") version "8.3.0"
}
val lwjglVersion: String by rootProject.extra
val jomlVersion: String by rootProject.extra
val jomlPrimitivesVersion: String by rootProject.extra
val lwjglNatives: String by rootProject.extra
sourceSets {
val main by getting {
java.srcDir("java")
}
}
dependencies {
annotationProcessor("org.projectlombok:lombok:1.18.34")
implementation("org.projectlombok:lombok:1.18.34")
annotationProcessor("javax.annotation:javax.annotation-api:1.3.2")
implementation("javax.annotation:javax.annotation-api:1.3.2")
compileOnly(project(":core", configuration = "shadow"))
implementation(platform("org.lwjgl:lwjgl-bom:$lwjglVersion"))
shadow(localGroovy())
shadow(gradleApi())
}
tasks.named<ShadowJar>("shadowJar") {
archiveFileName.set("dev.euph.engine.rendering.jar")
}
configurations {
shadow
}

View file

@ -1,7 +1,6 @@
package dev.euph.engine.util;
package dev.euph.engine.rendering;
public class Config {
public static final int MAX_LIGHTS = 6;
public static final int MAX_LIGHT_DISTANCE = 100;
}

View file

@ -0,0 +1,60 @@
package dev.euph.engine.rendering;
import dev.euph.engine.rendering.resources.Texture;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.experimental.Accessors;
import java.awt.*;
@Setter
@Getter
@Accessors(chain = true)
public class Material {
@Setter(AccessLevel.NONE)
protected final Shader shader;
private Texture albedoTexture;
private Color color;
public Material(Shader shader) {
this.shader = shader;
this.albedoTexture = null;
this.color = Color.WHITE;
}
public void useMaterial() {
shader.start();
prepareColor();
prepareAlbedoTexture();
}
private void prepareColor() {
int colorLocation = shader.getUniformLocation("color");
shader.loadVector4(colorLocation, new org.joml.Vector4f(
color.getRed() / 255f,
color.getGreen() / 255f,
color.getBlue() / 255f,
1f)
);
}
private void prepareAlbedoTexture() {
int useAlbedoTextureLocation = shader.getUniformLocation("useAlbedoTexture");
if (albedoTexture != null) {
int albedoTextureLocation = shader.getUniformLocation("albedoTexture");
shader.bindTexture2D(albedoTextureLocation, albedoTexture, Texture.ALBEDO_INDEX);
shader.loadBoolean(useAlbedoTextureLocation, true);
} else {
shader.loadBoolean(useAlbedoTextureLocation, false);
}
}
@SneakyThrows
public void cleanup() {
shader.deleteTexture(albedoTexture);
shader.close();
}
}

View file

@ -0,0 +1,50 @@
package dev.euph.engine.rendering;
import dev.euph.engine.rendering.resources.Texture;
import lombok.Setter;
@Setter
public class PBRMaterial extends Material {
private Texture normalTexture;
private Texture metallicTexture;
private Texture roughnessTexture;
private Texture aoTexture;
public PBRMaterial(Shader shader) {
super(shader);
}
@Override
public void useMaterial() {
super.useMaterial();
if (normalTexture != null) {
int normalTextureLocation = shader.getUniformLocation("normalTexture");
shader.bindTexture2D(normalTextureLocation, normalTexture, Texture.NORMAL_INDEX);
}
if (metallicTexture != null) {
int metallicTextureLocation = shader.getUniformLocation("metallicTexture");
shader.bindTexture2D(metallicTextureLocation, metallicTexture, Texture.METALLIC_INDEX);
}
if (roughnessTexture != null) {
int roughnessTextureLocation = shader.getUniformLocation("roughnessTexture");
shader.bindTexture2D(roughnessTextureLocation, roughnessTexture, Texture.ROUGHNESS_INDEX);
}
if (aoTexture != null) {
int aoTextureLocation = shader.getUniformLocation("aoTexture");
shader.bindTexture2D(aoTextureLocation, aoTexture, Texture.AMBIENT_OCCLUSION_INDEX);
}
}
@Override
public void cleanup() {
super.cleanup();
shader.deleteTexture(normalTexture);
shader.deleteTexture(metallicTexture);
shader.deleteTexture(roughnessTexture);
shader.deleteTexture(aoTexture);
}
}

View file

@ -0,0 +1,65 @@
package dev.euph.engine.rendering;
import org.joml.Matrix4f;
import org.joml.Vector2f;
import org.joml.Vector3f;
import org.joml.Vector4f;
public abstract class RenderPipeline {
protected static void startShader(Shader shader) {
shader.start();
}
protected static void stopShader(Shader shader) {
shader.stop();
}
protected static void bindShaderAttributes(Shader shader) {
shader.bindAttributes();
}
protected static void enableShaderAttributes(Shader shader) {
shader.enableAttributes();
}
protected static void disableShaderAttributes(Shader shader) {
shader.disableAttributes();
}
protected static void loadBoolean(Shader shader, int location, boolean value) {
shader.loadBoolean(location, value);
}
protected static void loadInt(Shader shader, int location, int value) {
shader.loadInt(location, value);
}
protected static void loadFloat(Shader shader, int location, float value) {
shader.loadFloat(location, value);
}
protected static void loadVector2(Shader shader, int location, Vector2f value) {
shader.loadVector2(location, value);
}
protected static void loadVector3(Shader shader, int location, Vector3f value) {
shader.loadVector3(location, value);
}
protected static void loadVector4(Shader shader, int location, Vector4f value) {
shader.loadVector4(location, value);
}
protected static void loadMatrix4(Shader shader, int location, Matrix4f value) {
shader.loadMatrix4(location, value);
}
protected static int getUniformLocation(Shader shader, String name) {
return shader.getUniformLocation(name);
}
public abstract void render();
public abstract void init();
}

View file

@ -0,0 +1,53 @@
package dev.euph.engine.rendering;
import dev.euph.engine.rendering.resources.Texture;
import lombok.Getter;
import org.joml.Vector2f;
import org.joml.Vector3f;
import org.joml.Vector4f;
import org.joml.Matrix4f;
public abstract class Shader implements AutoCloseable {
protected final int vertexShaderId;
protected final int fragmentShaderId;
@Getter
private final String name;
protected Shader(String name, int vertexShaderId, int fragmentShaderId) {
this.name = name;
this.vertexShaderId = vertexShaderId;
this.fragmentShaderId = fragmentShaderId;
}
protected abstract void start();
protected abstract void stop();
protected abstract void bindAttributes();
protected abstract void enableAttributes();
protected abstract void disableAttributes();
protected abstract int getUniformLocation(String name);
protected abstract void loadBoolean(int location, boolean value);
protected abstract void loadInt(int location, int value);
protected abstract void loadFloat(int location, float value);
protected abstract void loadVector2(int location, Vector2f value);
protected abstract void loadVector3(int location, Vector3f value);
protected abstract void loadVector4(int location, Vector4f value);
protected abstract void loadMatrix4(int location, Matrix4f value);
protected abstract void deleteTexture(Texture texture);
protected abstract void bindTexture2D(int location, Texture texture, int textureIndex);
protected abstract void bindTexture3D(int location, Texture texture, int textureIndex);
}

View file

@ -0,0 +1,31 @@
package dev.euph.engine.rendering;
import lombok.AllArgsConstructor;
import java.util.HashMap;
import java.util.Map;
@AllArgsConstructor
public class ShaderManager {
private final Map<String, Shader> shaders = new HashMap<>();
public void addShader(Shader shader) {
shaders.put(shader.getName(), shader);
}
public Shader getShader(String shaderName) {
return shaders.get(shaderName);
}
public void cleanup() {
shaders.values().forEach(shader -> {
try {
shader.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
shaders.clear();
}
}

View file

@ -0,0 +1,23 @@
package dev.euph.engine.rendering.components;
import dev.euph.engine.core.transform.Transform;
import dev.euph.engine.core.ecs.Component;
import dev.euph.engine.core.ecs.Requires;
import dev.euph.engine.core.window.Window;
import lombok.Getter;
import java.awt.*;
@Requires(Transform.class)
public class Camera extends Component {
public float fov = 90f;
public float nearPlane = 0.1f;
public float farPlane = 500f;
public Color backgroundColor = new Color(0, 200, 255);
@Getter
private Window window;
public Camera(Window window) {
this.window = window;
}
}

View file

@ -0,0 +1,17 @@
package dev.euph.engine.rendering.components;
import dev.euph.engine.core.transform.Transform;
import dev.euph.engine.core.ecs.Component;
import dev.euph.engine.core.ecs.Requires;
import dev.euph.engine.rendering.Material;
import dev.euph.engine.rendering.resources.Mesh;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
@Requires(Transform.class)
public class MeshRenderer extends Component {
private final Mesh mesh;
private final Material material;
}

View file

@ -0,0 +1,9 @@
package dev.euph.engine.rendering.components.lights;
import java.awt.*;
public class DirectionalLight extends LightSource {
public DirectionalLight(Color color) {
super(color, LightType.Directional);
}
}

View file

@ -0,0 +1,27 @@
package dev.euph.engine.rendering.components.lights;
import dev.euph.engine.core.ecs.Component;
import lombok.Getter;
import lombok.Setter;
import java.awt.*;
@Getter
public abstract class LightSource extends Component {
private final int type;
@Setter
private Color color;
public LightSource(Color color, int type) {
this.color = color;
this.type = type;
}
public static class LightType {
public static final int Directional = 0;
public static final int Point = 1;
public static final int Spot = 2;
public static final int Area = 3;
}
}

View file

@ -0,0 +1,17 @@
package dev.euph.engine.rendering.components.lights;
import dev.euph.engine.core.transform.Transform;
import dev.euph.engine.core.ecs.Requires;
import org.joml.Vector3f;
import java.awt.*;
@Requires(Transform.class)
public class PointLight extends LightSource {
public Vector3f attenuation;
public PointLight(Color color, Vector3f attenuation) {
super(color, LightType.Point);
this.attenuation = attenuation;
}
}

View file

@ -1,18 +1,18 @@
package dev.euph.engine.ecs.components.lights;
package dev.euph.engine.rendering.components.lights;
import dev.euph.engine.core.transform.Transform;
import dev.euph.engine.core.ecs.Requires;
import org.joml.Vector3f;
import java.awt.*;
@Requires(Transform.class)
public class SpotLight extends LightSource {
//region Fields
public float coneAngle;
public float hardCutoff;
public float softCutoff;
private Vector3f attenuation;
//endregion
public Vector3f attenuation;
//region Constructor
public SpotLight(Color color, Vector3f attenuation, float coneAngle, float hardCutoff, float softCutoff) {
super(color, LightType.Spot);
this.attenuation = attenuation;
@ -20,5 +20,4 @@ public class SpotLight extends LightSource{
this.hardCutoff = hardCutoff;
this.softCutoff = softCutoff;
}
//endregion
}

View file

@ -0,0 +1,7 @@
package dev.euph.engine.rendering.resources;
public interface Mesh {
void bind();
void unbind();
int getVertexCount();
}

View file

@ -0,0 +1,23 @@
package dev.euph.engine.rendering.resources;
import lombok.Getter;
@Getter
public class Texture {
public final static int ALBEDO_INDEX = 0;
public final static int NORMAL_INDEX = 1;
public final static int METALLIC_INDEX = 2;
public final static int ROUGHNESS_INDEX = 3;
public final static int AMBIENT_OCCLUSION_INDEX = 4;
private final int id;
private final int height;
private final int width;
public Texture(int id, int width, int height) {
this.id = id;
this.width = width;
this.height = height;
}
}

View file

@ -1,23 +1,18 @@
package dev.euph.engine.math;
package dev.euph.engine.rendering.utils;
import dev.euph.engine.ecs.components.Camera;
import dev.euph.engine.managers.Window;
import dev.euph.engine.core.window.Window;
import dev.euph.engine.rendering.components.Camera;
import org.joml.Matrix4f;
import org.lwjgl.BufferUtils;
import java.nio.IntBuffer;
import static org.lwjgl.glfw.GLFW.glfwGetWindowSize;
public class ProjectionMatrix extends Matrix4f {
public ProjectionMatrix(Camera camera) {
super();
calcualteProjection(camera.getWindow(), camera.fov, camera.nearPlane, camera.farPlane);
calculateProjection(camera.getWindow(), camera.fov, camera.nearPlane, camera.farPlane);
}
private void calcualteProjection(Window window, float fov, float nearPlane, float farPlane) {
float aspectRatio = (float) window.getInitalWidth() / (float) window.getInitalHeight();
private void calculateProjection(Window window, float fov, float nearPlane, float farPlane) {
float aspectRatio = (float) window.getWidth() / (float) window.getHeight();
float y_scale = (float) (1f / Math.tan(Math.toRadians(fov / 2f))) * aspectRatio;
float x_scale = y_scale / aspectRatio;

View file

@ -1,7 +1,7 @@
package dev.euph.engine.math;
package dev.euph.engine.rendering.utils;
import dev.euph.engine.ecs.components.Camera;
import dev.euph.engine.ecs.components.Transform;
import dev.euph.engine.core.transform.Transform;
import dev.euph.engine.rendering.components.Camera;
import org.joml.Math;
import org.joml.Matrix4f;
import org.joml.Vector3f;

View file

@ -0,0 +1,46 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
repositories {
mavenCentral()
}
plugins {
java
id("com.gradleup.shadow") version "8.3.0"
}
val lwjglVersion: String by rootProject.extra
val jomlVersion: String by rootProject.extra
val jomlPrimitivesVersion: String by rootProject.extra
val lwjglNatives: String by rootProject.extra
sourceSets {
val main by getting {
java.srcDir("java")
}
}
dependencies {
annotationProcessor("org.projectlombok:lombok:1.18.34")
implementation("org.projectlombok:lombok:1.18.34")
annotationProcessor("javax.annotation:javax.annotation-api:1.3.2")
implementation("javax.annotation:javax.annotation-api:1.3.2")
compileOnly(project(":core", configuration = "shadow"))
implementation(platform("org.lwjgl:lwjgl-bom:$lwjglVersion"))
implementation("org.lwjgl", "lwjgl-assimp")
runtimeOnly("org.lwjgl", "lwjgl-assimp", classifier = lwjglNatives)
shadow(localGroovy())
shadow(gradleApi())
}
tasks.named<ShadowJar>("shadowJar") {
archiveFileName.set("dev.euph.engine.resources.jar")
}
configurations {
shadow
}

7
TestGame/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
.gradle/
gradle/
gradlew
gradlew.bat
build/
.idea/

73
TestGame/build.gradle.kts Normal file
View file

@ -0,0 +1,73 @@
plugins {
application
java
}
repositories {
mavenCentral()
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(22))
}
}
sourceSets {
main {
java {
srcDir("src")
}
resources {
srcDir("res")
}
}
}
application {
mainClass.set("dev.euph.game.Main")
}
val lwjglVersion = "3.3.4"
val jomlVersion = "1.10.7"
val jomlPrimitivesVersion = "1.10.0"
val lwjglNatives = Pair(
System.getProperty("os.name")!!,
System.getProperty("os.arch")!!
).let { (name, arch) ->
when {
arrayOf("Linux", "SunOS", "Unit").any { name.startsWith(it) } ->
if (arrayOf("arm", "aarch64").any { arch.startsWith(it) })
"natives-linux${if (arch.contains("64") || arch.startsWith("armv8")) "-arm64" else "-arm32"}"
else if (arch.startsWith("ppc"))
"natives-linux-ppc64le"
else if (arch.startsWith("riscv"))
"natives-linux-riscv64"
else
"natives-linux"
arrayOf("Mac OS X", "Darwin").any { name.startsWith(it) } ->
"natives-macos${if (arch.startsWith("aarch64")) "-arm64" else ""}"
arrayOf("Windows").any { name.startsWith(it) } ->
if (arch.contains("64"))
"natives-windows${if (arch.startsWith("aarch64")) "-arm64" else ""}"
else
"natives-windows-x86"
else ->
throw Error("Unrecognized or unsupported platform. Please set \"lwjglNatives\" manually")
}
}
extra["lwjglVersion"] = lwjglVersion
extra["jomlVersion"] = jomlVersion
extra["jomlPrimitivesVersion"] = jomlPrimitivesVersion
extra["lwjglNatives"] = lwjglNatives
dependencies {
implementation(project(":core", configuration = "shadow"))
implementation(project(":rendering", configuration = "shadow"))
implementation(project(":opengl", configuration = "shadow"))
}

View file

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View file

@ -0,0 +1,8 @@
rootProject.name = "Game"
include(":core")
project(":core").projectDir = file("../Engine/src/core")
include(":rendering")
project(":rendering").projectDir = file("../Engine/src/rendering")
include(":opengl")
project(":opengl").projectDir = file("../Engine/src/opengl")

View file

@ -1,16 +1,19 @@
package dev.euph.game;
import dev.euph.engine.ecs.Component;
import dev.euph.engine.ecs.components.Camera;
import dev.euph.engine.ecs.components.Transform;
import dev.euph.engine.core.transform.Transform;
import dev.euph.engine.core.ecs.Component;
import dev.euph.engine.core.ecs.Requires;
import dev.euph.engine.rendering.components.Camera;
import org.joml.Vector2f;
import org.joml.Vector3f;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWKeyCallback;
import lombok.NoArgsConstructor;
import static java.lang.Math.max;
import static java.lang.Math.min;
@NoArgsConstructor
@Requires({Transform.class, Camera.class})
public class CameraController extends Component {
Camera camera;
@ -24,11 +27,6 @@ public class CameraController extends Component {
private Vector2f prevMousePos;
private Vector2f inputVectorWASD = new Vector2f();
private boolean justCapturedMouse = true;
public CameraController() {
super();
requireComponent(Transform.class);
requireComponent(Camera.class);
}
@Override
public void OnReady() {

View file

@ -0,0 +1,100 @@
package dev.euph.game;
import dev.euph.engine.core.ecs.Entity;
import dev.euph.engine.core.ecs.Scene;
import dev.euph.engine.core.transform.Transform;
import dev.euph.engine.core.window.Window;
import dev.euph.engine.opengl.*;
import dev.euph.engine.rendering.Material;
import dev.euph.engine.rendering.ShaderManager;
import dev.euph.engine.rendering.components.Camera;
import dev.euph.engine.rendering.components.MeshRenderer;
import dev.euph.engine.rendering.components.lights.DirectionalLight;
import dev.euph.engine.rendering.components.lights.PointLight;
import dev.euph.engine.rendering.resources.Mesh;
import dev.euph.engine.rendering.resources.Texture;
import org.joml.Vector3f;
import java.awt.*;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.atomic.AtomicBoolean;
public class Main {
public static void main(String[] args) throws IOException {
Window window = new GLWindow(1200, 720, "Test Window");
window.setVsync(1);
ShaderManager shaderManager = loadShaders();
Scene scene = loadScene(window, shaderManager);
Entity camera = scene.getByName("Camera").stream().findFirst().orElse(null);
Entity Model = scene.getByName("Model").stream().findFirst().orElse(null);
GLRenderer forwardRenderer = new GLRenderer(scene, shaderManager);
forwardRenderer.activeCamera = camera.getComponent(Camera.class);
forwardRenderer.init();
float rotationAngle = 0;
float rotationSpeed = 20f;
scene.start();
AtomicBoolean running = new AtomicBoolean(true);
window.addCloseCallback(() -> {
running.set(false);
});
while (running.get()) {
scene.update(window.getTime().getDeltaTime());
rotationAngle += rotationSpeed * window.getTime().getDeltaTime();
rotationAngle %= 360;
Model.getComponent(Transform.class).setRotation(new Vector3f(0, rotationAngle, 0));
forwardRenderer.render();
window.update();
}
shaderManager.cleanup();
window.destroy();
}
private static ShaderManager loadShaders() throws IOException {
ShaderManager shaderManager = new ShaderManager();
URL fragmentShader = Main.class.getResource("/dev/euph/game/shader/default.fs.glsl");
URL vertexShader = Main.class.getResource("/dev/euph/game/shader/default.vs.glsl");
assert vertexShader != null;
assert fragmentShader != null;
shaderManager.addShader(new GLShader(
"default",
Main.class.getResourceAsStream("/dev/euph/game/shader/default.vs.glsl"),
Main.class.getResourceAsStream("/dev/euph/game/shader/default.fs.glsl")
));
return shaderManager;
}
private static Scene loadScene(Window window, ShaderManager shaderManager) {
Scene scene = new Scene();
Entity cameraEntity = new Entity("Camera")
.addComponent(Transform.FromPosition(0, 0, 3))
.addComponent(new Camera(window))
.addComponent(new CameraController())
.addToScene(scene);
Entity light1 = new Entity("DirectionalLight")
.addComponent(Transform.FromRotation(-0.5f, -1.0f, -0.5f))
.addComponent(new DirectionalLight(Color.WHITE))
.addToScene(scene);
Entity light2 = new Entity("PointLight")
.addComponent(Transform.FromRotation(-0.5f, -1.0f, -0.5f))
.addComponent(new PointLight(Color.red, new Vector3f(10)))
.addToScene(scene);
Mesh modelMesh = GLMeshLoader.loadTexturedMesh(Main.class.getResource("/dev/euph/game/models/human_rigged.obj"));
Texture uvTexture = GLTextureLoader.loadTexture(Main.class.getResource("/dev/euph/game/textures/uv.png"));
Material modelMaterial = new Material(shaderManager.getShader("default"))
.setColor(new Color(7, 77, 255, 255))
.setAlbedoTexture(uvTexture);
Entity modelEntity = new Entity("Model")
.addComponent(Transform.FromPosition(0, -4, -6).setScale(3))
.addComponent(new MeshRenderer(modelMesh, modelMaterial))
.addToScene(scene);
return scene;
}
}

View file

@ -1,6 +1,7 @@
package dev.euph.game;
import dev.euph.engine.datastructs.octree.Octree;
import dev.euph.engine.core.data.octree.Octree;
import dev.euph.engine.core.data.octree.OctreeNode;
import org.joml.Vector3f;
import javax.imageio.ImageIO;
@ -15,7 +16,11 @@ public class OctreeTest {
public static void main(String[] args) {
// Create an octree with a center at (0, 0, 0) and a half-size of 20 units
Octree<String> octree = new Octree<>(8, new Vector3f(0, 0, 0), 20);
Octree<String> octree = new Octree<>(new OctreeNode<String>(
8,
new Vector3f(),
20
));
// Generate clustered noise for more interesting patterns
Random random = new Random();

View file

@ -1,7 +1,7 @@
package dev.euph.game;
import dev.euph.engine.datastructs.pipeline.Pipeline;
import dev.euph.engine.datastructs.pipeline.PipelineStage;
import dev.euph.engine.core.data.pipeline.Pipeline;
import dev.euph.engine.core.data.pipeline.PipelineStage;
public class PipelineTest {
public static void main(String[] args) {

View file

@ -1,66 +0,0 @@
val lwjglVersion = "3.3.3"
val jomlVersion = "1.10.5"
group = "dev.euph"
version = "1.0-SNAPSHOT"
sourceSets {
main {
java.srcDirs("src")
resources.srcDir("res")
}
}
plugins {
java
application
}
val lwjglNatives = Pair(
System.getProperty("os.name")!!,
System.getProperty("os.arch")!!
).let { (name, arch) ->
when {
arrayOf("Linux", "FreeBSD", "SunOS", "Unit").any { name.startsWith(it) } ->
if (arrayOf("arm", "aarch64").any { arch.startsWith(it) })
"natives-linux${if (arch.contains("64") || arch.startsWith("armv8")) "-arm64" else "-arm32"}"
else if (arch.startsWith("ppc"))
"natives-linux-ppc64le"
else if (arch.startsWith("riscv"))
"natives-linux-riscv64"
else
"natives-linux"
arrayOf("Mac OS X", "Darwin").any { name.startsWith(it) } ->
"natives-macos${if (arch.startsWith("aarch64")) "-arm64" else ""}"
arrayOf("Windows").any { name.startsWith(it) } ->
if (arch.contains("64"))
"natives-windows${if (arch.startsWith("aarch64")) "-arm64" else ""}"
else
"natives-windows-x86"
else -> throw Error("Unrecognized or unsupported platform. Please set \"lwjglNatives\" manually")
}
}
repositories {
mavenCentral()
}
dependencies {
implementation(platform("org.lwjgl:lwjgl-bom:$lwjglVersion"))
implementation("org.lwjgl", "lwjgl")
implementation("org.lwjgl", "lwjgl-assimp")
implementation("org.lwjgl", "lwjgl-glfw")
implementation("org.lwjgl", "lwjgl-openal")
implementation("org.lwjgl", "lwjgl-opengl")
implementation("org.lwjgl", "lwjgl-remotery")
implementation("org.lwjgl", "lwjgl-stb")
runtimeOnly("org.lwjgl", "lwjgl", classifier = lwjglNatives)
runtimeOnly("org.lwjgl", "lwjgl-assimp", classifier = lwjglNatives)
runtimeOnly("org.lwjgl", "lwjgl-glfw", classifier = lwjglNatives)
runtimeOnly("org.lwjgl", "lwjgl-openal", classifier = lwjglNatives)
runtimeOnly("org.lwjgl", "lwjgl-opengl", classifier = lwjglNatives)
runtimeOnly("org.lwjgl", "lwjgl-remotery", classifier = lwjglNatives)
runtimeOnly("org.lwjgl", "lwjgl-stb", classifier = lwjglNatives)
implementation("org.joml", "joml", jomlVersion)
}

View file

@ -1,7 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

248
gradlew vendored
View file

@ -1,248 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View file

@ -1,175 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,59 +0,0 @@
//
// Copyright 2014-2018 Celtoys Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#include <Foundation/NSThread.h>
#include <Foundation/NSDictionary.h>
#include <Foundation/NSString.h>
#import <Metal/Metal.h>
// Store command buffer in thread-local so that each thread can point to its own
static void SetCommandBuffer(id command_buffer)
{
NSMutableDictionary* thread_data = [[NSThread currentThread] threadDictionary];
thread_data[@"rmtMTLCommandBuffer"] = command_buffer;
}
static id GetCommandBuffer()
{
NSMutableDictionary* thread_data = [[NSThread currentThread] threadDictionary];
return thread_data[@"rmtMTLCommandBuffer"];
}
extern "C" void _rmt_BindMetal(id command_buffer)
{
SetCommandBuffer(command_buffer);
}
extern "C" void _rmt_UnbindMetal()
{
SetCommandBuffer(0);
}
// Needs to be in the same lib for this to work
extern "C" unsigned long long rmtMetal_usGetTime();
static void SetTimestamp(void* data)
{
*((unsigned long long*)data) = rmtMetal_usGetTime();
}
extern "C" void rmtMetal_MeasureCommandBuffer(unsigned long long* out_start, unsigned long long* out_end, unsigned int* out_ready)
{
id command_buffer = GetCommandBuffer();
[command_buffer addScheduledHandler:^(id <MTLCommandBuffer>){ SetTimestamp(out_start); }];
[command_buffer addCompletedHandler:^(id <MTLCommandBuffer>){ SetTimestamp(out_end); *out_ready = 1; }];
}

View file

@ -1,232 +0,0 @@
Remotery
--------
[![Build](https://github.com/Celtoys/Remotery/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/Celtoys/Remotery/actions/workflows/build.yml)
A realtime CPU/GPU profiler hosted in a single C file with a viewer that runs in a web browser.
![screenshot](screenshot.png?raw=true)
Features:
* Lightweight instrumentation of multiple threads running on the CPU.
* Web viewer that runs in Chrome, Firefox and Safari; on Desktops, Mobiles or Tablets.
* GPU UI rendering, bypassing the DOM completely, for real-time 60hz viewer updates at 10,000x the performance.
* Automatic thread sampler that tells you what processor cores your threads are running on without requiring Administrator privileges.
* Drop saved traces onto the Remotery window to load historical runs for inspection.
* Console output for logging text.
* Console input for sending commands to your game.
* A Property API for recording named/typed values over time, alongside samples.
* Profiles itself and shows how it's performing in the viewer.
Supported Profiling Platforms:
* Windows 7/8/10/11/UWP (Hololens), Linux, OSX, iOS, Android, Xbox One/Series, Free BSD.
Supported GPU Profiling APIS:
* D3D 11/12, OpenGL, CUDA, Metal.
Compiling
---------
* Windows (MSVC) - add lib/Remotery.c and lib/Remotery.h to your program. Set include
directories to add Remotery/lib path. The required library ws2_32.lib should be picked
up through the use of the #pragma comment(lib, "ws2_32.lib") directive in Remotery.c.
* Mac OS X (XCode) - simply add lib/Remotery.c, lib/Remotery.h and lib/Remotery.mm to your program.
* Linux (GCC) - add the source in lib folder. Compilation of the code requires -pthreads for
library linkage. For example to compile the same run: cc lib/Remotery.c sample/sample.c
-I lib -pthread -lm
* FreeBSD - the easiest way is to take a look at the official port
([devel/remotery](https://www.freshports.org/devel/remotery/)) and modify the port's
Makefile if needed. There is also a package available via `pkg install remotery`.
You can define some extra macros to modify what features are compiled into Remotery:
Macro Default Description
RMT_ENABLED 1 Disable this to not include any bits of Remotery in your build
RMT_USE_TINYCRT 0 Used by the Celtoys TinyCRT library (not released yet)
RMT_USE_CUDA 0 Assuming CUDA headers/libs are setup, allow CUDA profiling
RMT_USE_D3D11 0 Assuming Direct3D 11 headers/libs are setup, allow D3D11 GPU profiling
RMT_USE_D3D12 0 Allow D3D12 GPU profiling
RMT_USE_OPENGL 0 Allow OpenGL GPU profiling (dynamically links OpenGL libraries on available platforms)
RMT_USE_METAL 0 Allow Metal profiling of command buffers
Basic Use
---------
See the sample directory for further examples. A quick example:
int main()
{
// Create the main instance of Remotery.
// You need only do this once per program.
Remotery* rmt;
rmt_CreateGlobalInstance(&rmt);
// Explicit begin/end for C
{
rmt_BeginCPUSample(LogText, 0);
rmt_LogText("Time me, please!");
rmt_EndCPUSample();
}
// Scoped begin/end for C++
{
rmt_ScopedCPUSample(LogText, 0);
rmt_LogText("Time me, too!");
}
// Destroy the main instance of Remotery.
rmt_DestroyGlobalInstance(rmt);
}
Running the Viewer
------------------
Double-click or launch `vis/index.html` from the browser.
Sampling CUDA GPU activity
--------------------------
Remotery allows for profiling multiple threads of CUDA execution using different asynchronous streams
that must all share the same context. After initialising both Remotery and CUDA you need to bind the
two together using the call:
rmtCUDABind bind;
bind.context = m_Context;
bind.CtxSetCurrent = &cuCtxSetCurrent;
bind.CtxGetCurrent = &cuCtxGetCurrent;
bind.EventCreate = &cuEventCreate;
bind.EventDestroy = &cuEventDestroy;
bind.EventRecord = &cuEventRecord;
bind.EventQuery = &cuEventQuery;
bind.EventElapsedTime = &cuEventElapsedTime;
rmt_BindCUDA(&bind);
Explicitly pointing to the CUDA interface allows Remotery to be included anywhere in your project without
need for you to link with the required CUDA libraries. After the bind completes you can safely sample any
CUDA activity:
CUstream stream;
// Explicit begin/end for C
{
rmt_BeginCUDASample(UnscopedSample, stream);
// ... CUDA code ...
rmt_EndCUDASample(stream);
}
// Scoped begin/end for C++
{
rmt_ScopedCUDASample(ScopedSample, stream);
// ... CUDA code ...
}
Remotery supports only one context for all threads and will use cuCtxGetCurrent and cuCtxSetCurrent to
ensure the current thread has the context you specify in rmtCUDABind.context.
Sampling Direct3D 11 GPU activity
---------------------------------
Remotery allows sampling of D3D11 GPU activity on multiple devices on multiple threads. After initialising Remotery, you need to bind it to D3D11 with a single call from the thread that owns the device context:
// Parameters are ID3D11Device* and ID3D11DeviceContext*
rmt_BindD3D11(d3d11_device, d3d11_context);
Sampling is then a simple case of:
// Explicit begin/end for C
{
rmt_BeginD3D11Sample(UnscopedSample);
// ... D3D code ...
rmt_EndD3D11Sample();
}
// Scoped begin/end for C++
{
rmt_ScopedD3D11Sample(ScopedSample);
// ... D3D code ...
}
Subsequent sampling calls from the same thread will use that device/context combination. When you shutdown your D3D11 device and context, ensure you notify Remotery before shutting down Remotery itself:
rmt_UnbindD3D11();
Sampling OpenGL GPU activity
----------------------------
Remotery allows sampling of GPU activity on your main OpenGL context. After initialising Remotery, you need
to bind it to OpenGL with the single call:
rmt_BindOpenGL();
Sampling is then a simple case of:
// Explicit begin/end for C
{
rmt_BeginOpenGLSample(UnscopedSample);
// ... OpenGL code ...
rmt_EndOpenGLSample();
}
// Scoped begin/end for C++
{
rmt_ScopedOpenGLSample(ScopedSample);
// ... OpenGL code ...
}
Support for multiple contexts can be added pretty easily if there is demand for the feature. When you shutdown
your OpenGL device and context, ensure you notify Remotery before shutting down Remotery itself:
rmt_UnbindOpenGL();
Sampling Metal GPU activity
---------------------------
Remotery can sample Metal command buffers issued to the GPU from multiple threads. As the Metal API does not
support finer grained profiling, samples will return only the timing of the bound command buffer, irrespective
of how many you issue. As such, make sure you bind and sample the command buffer for each call site:
rmt_BindMetal(mtl_command_buffer);
rmt_ScopedMetalSample(command_buffer_name);
The C API supports begin/end also:
rmt_BindMetal(mtl_command_buffer);
rmt_BeginMetalSample(command_buffer_name);
...
rmt_EndMetalSample();
Applying Configuration Settings
-------------------------------
Before creating your Remotery instance, you can configure its behaviour by retrieving its settings object:
rmtSettings* settings = rmt_Settings();
Some important settings are:
// Redirect any Remotery allocations to your own malloc/free, with an additional context pointer
// that gets passed to your callbacks.
settings->malloc;
settings->free;
settings->mm_context;
// Specify an input pipelineStage that receives text input from the Remotery console, with an additional
// context pointer that gets passed to your callback.
// The pipelineStage will be called from the Remotery thread so synchronization with a mutex or atomics
// might be needed to avoid race conditions with your threads.
settings->input_handler;
settings->input_handler_context;

View file

@ -1,184 +0,0 @@
#include <stdlib.h>
#include <math.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include "../lib/Remotery.h"
#include <assert.h>
rmt_PropertyDefine_Group(Game, "Game Properties");
rmt_PropertyDefine_Bool(WasUpdated, RMT_FALSE, FrameReset, "Was the game loop executed this frame?", &Game);
rmt_PropertyDefine_U32(RecursiveDepth, 0, FrameReset, "How deep did we go in recursiveFunction?", &Game);
rmt_PropertyDefine_F32(Accumulated, 0, FrameReset, "What was the latest value?", &Game);
rmt_PropertyDefine_U32(FrameCounter, 0, NoFlags, "What is the current frame number?", &Game);
void aggregateFunction() {
rmt_BeginCPUSample(aggregate, RMTSF_Aggregate);
rmt_EndCPUSample();
}
void recursiveFunction(int depth) {
rmt_PropertySet_U32(RecursiveDepth, depth);
rmt_BeginCPUSample(recursive, RMTSF_Recursive);
if (depth < 5) {
recursiveFunction(depth + 1);
}
rmt_EndCPUSample();
}
double delay() {
int i, end;
double j = 0;
rmt_BeginCPUSample(delay, 0);
for( i = 0, end = rand()/100; i < end; ++i ) {
double v = sin(i);
j += v;
rmt_PropertyAdd_F32(Accumulated, v);
}
recursiveFunction(0);
aggregateFunction();
aggregateFunction();
aggregateFunction();
rmt_EndCPUSample();
return j;
}
void printIndent(int indent)
{
int i;
for (i = 0; i < indent; ++i) {
printf(" ");
}
}
void printSample(rmtSample* sample, int indent)
{
const char* name = rmt_SampleGetName(sample);
rmtU32 callcount = rmt_SampleGetCallCount(sample);
rmtU64 time = rmt_SampleGetTime(sample);
rmtU64 self_time = rmt_SampleGetSelfTime(sample);
rmtSampleType type = rmt_SampleGetType(sample);
rmtU8 r, g, b;
rmt_SampleGetColour(sample, &r, &g, &b);
printIndent(indent); printf("%s %u time: %llu self: %llu type: %d color: 0x%02x%02x%02x\n", name, callcount, time, self_time, type, r, g, b);
}
void printTree(rmtSample* sample, int indent)
{
rmtSampleIterator iter;
printSample(sample, indent);
rmt_IterateChildren(&iter, sample);
while (rmt_IterateNext(&iter)) {
printTree(iter.sample, indent+1);
}
}
void dumpTree(void* ctx, rmtSampleTree* sample_tree)
{
rmtSample* root = rmt_SampleTreeGetRootSample(sample_tree);
const char* thread_name = rmt_SampleTreeGetThreadName(sample_tree);
if (strcmp("Remotery", thread_name) == 0)
{
return; // to minimize the verbosity in this example
}
printf("// ******************** DUMP TREE: %s ************************\n", thread_name);
printTree(root, 0);
}
void printProperty(rmtProperty* property, int indent)
{
rmtPropertyIterator iter;
const char* name = rmt_PropertyGetName(property);
rmtPropertyType type = rmt_PropertyGetType(property);
rmtPropertyValue value = rmt_PropertyGetValue(property);
printIndent(indent); printf("%s: ", name);
switch(type)
{
case RMT_PropertyType_rmtBool: printf("%s\n", value.Bool ? "true":"false"); break;
case RMT_PropertyType_rmtS32: printf("%d\n", value.S32); break;
case RMT_PropertyType_rmtU32: printf("%u\n", value.U32); break;
case RMT_PropertyType_rmtF32: printf("%f\n", value.F32); break;
case RMT_PropertyType_rmtS64: printf("%lld\n", value.S64); break;
case RMT_PropertyType_rmtU64: printf("%llu\n", value.U64); break;
case RMT_PropertyType_rmtF64: printf("%g\n", value.F64); break;
case RMT_PropertyType_rmtGroup: printf("\n"); break;
default: break;
};
rmt_PropertyIterateChildren(&iter, property);
while (rmt_PropertyIterateNext(&iter)) {
printProperty(iter.property, indent + 1);
}
}
void dumpProperties(void* ctx, rmtProperty* root)
{
rmtPropertyIterator iter;
printf("// ******************** DUMP PROPERTIES: ************************\n");
rmt_PropertyIterateChildren(&iter, root);
while (rmt_PropertyIterateNext(&iter)) {
printProperty(iter.property, 0);
}
}
int sig = 0;
/// Allow to close cleanly with ctrl + c
void sigintHandler(int sig_num) {
sig = sig_num;
printf("Interrupted\n");
}
int main() {
Remotery* rmt;
rmtError error;
signal(SIGINT, sigintHandler);
rmtSettings* settings = rmt_Settings();
if (settings)
{
settings->sampletree_handler = dumpTree;
settings->sampletree_context = 0;
settings->snapshot_callback = dumpProperties;
settings->snapshot_context = 0;
}
error = rmt_CreateGlobalInstance(&rmt);
if( RMT_ERROR_NONE != error) {
printf("Error launching Remotery %d\n", error);
return -1;
}
int max_count = 5;
while (sig == 0 && --max_count > 0) {
rmt_LogText("start profiling");
delay();
rmt_LogText("end profiling");
rmt_PropertySet_Bool(WasUpdated, RMT_TRUE);
rmt_PropertyAdd_U32(FrameCounter, 1);
rmt_PropertySnapshotAll();
rmt_PropertyFrameResetAll();
}
rmt_DestroyGlobalInstance(rmt);
printf("Cleaned up and quit\n");
return 0;
}

View file

@ -1,64 +0,0 @@
#include <stdlib.h>
#include <math.h>
#include <signal.h>
#include <stdio.h>
#include "../lib/Remotery.h"
void aggregateFunction() {
rmt_BeginCPUSample(aggregate, RMTSF_Aggregate);
rmt_EndCPUSample();
}
void recursiveFunction(int depth) {
rmt_BeginCPUSample(recursive, RMTSF_Recursive);
if (depth < 5) {
recursiveFunction(depth + 1);
}
rmt_EndCPUSample();
}
double delay() {
int i, end;
double j = 0;
rmt_BeginCPUSample(delay, 0);
for( i = 0, end = rand()/100; i < end; ++i ) {
j += sin(i);
}
recursiveFunction(0);
aggregateFunction();
aggregateFunction();
aggregateFunction();
rmt_EndCPUSample();
return j;
}
int sig = 0;
/// Allow to close cleanly with ctrl + c
void sigintHandler(int sig_num) {
sig = sig_num;
printf("Interrupted\n");
}
int main( ) {
Remotery *rmt;
rmtError error;
signal(SIGINT, sigintHandler);
error = rmt_CreateGlobalInstance(&rmt);
if( RMT_ERROR_NONE != error) {
printf("Error launching Remotery %d\n", error);
return -1;
}
while (sig == 0) {
rmt_LogText("start profiling");
delay();
rmt_LogText("end profiling");
}
rmt_DestroyGlobalInstance(rmt);
printf("Cleaned up and quit\n");
return 0;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

View file

@ -1,218 +0,0 @@
Console = (function()
{
var BORDER = 10;
var HEIGHT = 200;
function Console(wm, server)
{
// Create the window and its controls
this.Window = wm.AddWindow("Console", 10, 10, 100, 100);
this.PageContainer = this.Window.AddControlNew(new WM.Container(10, 10, 400, 160));
DOM.Node.AddClass(this.PageContainer.Node, "ConsoleText");
this.AppContainer = this.Window.AddControlNew(new WM.Container(10, 10, 400, 160));
DOM.Node.AddClass(this.AppContainer.Node, "ConsoleText");
this.UserInput = this.Window.AddControlNew(new WM.EditBox(10, 5, 400, 30, "Input", ""));
this.UserInput.SetChangeHandler(Bind(ProcessInput, this));
this.Window.ShowNoAnim();
// This accumulates log text as fast as is required
this.PageTextBuffer = "";
this.PageTextUpdatePending = false;
this.AppTextBuffer = "";
this.AppTextUpdatePending = false;
// Setup command history control
this.CommandHistory = LocalStore.Get("App", "Global", "CommandHistory", [ ]);
this.CommandIndex = 0;
this.MaxNbCommands = 10000;
DOM.Event.AddHandler(this.UserInput.EditNode, "keydown", Bind(OnKeyPress, this));
DOM.Event.AddHandler(this.UserInput.EditNode, "focus", Bind(OnFocus, this));
// At a much lower frequency this will update the console window
window.setInterval(Bind(UpdateHTML, this), 500);
// Setup log requests from the server
this.Server = server;
server.SetConsole(this);
server.AddMessageHandler("LOGM", Bind(OnLog, this));
this.Window.SetOnResize(Bind(OnUserResize, this));
}
Console.prototype.Log = function(text)
{
this.PageTextBuffer = LogText(this.PageTextBuffer, text);
this.PageTextUpdatePending = true;
}
Console.prototype.WindowResized = function(width, height)
{
// Place window
this.Window.SetPosition(BORDER, height - BORDER - 200);
this.Window.SetSize(width - 2 * BORDER, HEIGHT);
ResizeInternals(this);
}
Console.prototype.TriggerUpdate = function()
{
this.AppTextUpdatePending = true;
}
function OnLog(self, socket, data_view_reader)
{
var text = data_view_reader.GetString();
self.AppTextBuffer = LogText(self.AppTextBuffer, text);
// Don't register text as updating if disconnected as this implies a trace is being loaded, which we want to speed up
if (self.Server.Connected())
{
self.AppTextUpdatePending = true;
}
}
function LogText(existing_text, new_text)
{
// Filter the text a little to make it safer
if (new_text == null)
new_text = "NULL";
// Find and convert any HTML entities, ensuring the browser doesn't parse any embedded HTML code
// This also allows the log to contain arbitrary C++ code (e.g. assert comparison operators)
new_text = Convert.string_to_html_entities(new_text);
// Prefix date and end with new line
var d = new Date();
new_text = "[" + d.toLocaleTimeString() + "] " + new_text + "<br>";
// Append to local text buffer and ensure clip the oldest text to ensure a max size
existing_text = existing_text + new_text;
var max_len = 100 * 1024;
var len = existing_text.length;
if (len > max_len)
existing_text = existing_text.substr(len - max_len, max_len);
return existing_text;
}
function OnUserResize(self, evt)
{
ResizeInternals(self);
}
function ResizeInternals(self)
{
// Place controls
var parent_size = self.Window.Size;
var mid_w = parent_size[0] / 3;
self.UserInput.SetPosition(BORDER, parent_size[1] - 2 * BORDER - 30);
self.UserInput.SetSize(parent_size[0] - 100, 18);
var output_height = self.UserInput.Position[1] - 2 * BORDER;
self.PageContainer.SetPosition(BORDER, BORDER);
self.PageContainer.SetSize(mid_w - 2 * BORDER, output_height);
self.AppContainer.SetPosition(mid_w, BORDER);
self.AppContainer.SetSize(parent_size[0] - mid_w - BORDER, output_height);
}
function UpdateHTML(self)
{
// Reset the current text buffer as html
if (self.PageTextUpdatePending)
{
var page_node = self.PageContainer.Node;
page_node.innerHTML = self.PageTextBuffer;
page_node.scrollTop = page_node.scrollHeight;
self.PageTextUpdatePending = false;
}
if (self.AppTextUpdatePending)
{
var app_node = self.AppContainer.Node;
app_node.innerHTML = self.AppTextBuffer;
app_node.scrollTop = app_node.scrollHeight;
self.AppTextUpdatePending = false;
}
}
function ProcessInput(self, node)
{
// Send the message exactly
var msg = node.value;
self.Server.Send("CONI" + msg);
// Emit to console and clear
self.Log("> " + msg);
self.UserInput.SetValue("");
// Keep track of recently issued commands, with an upper bound
self.CommandHistory.push(msg);
var extra_commands = self.CommandHistory.length - self.MaxNbCommands;
if (extra_commands > 0)
self.CommandHistory.splice(0, extra_commands);
// Set command history index to the most recent command
self.CommandIndex = self.CommandHistory.length;
// Backup to local store
LocalStore.Set("App", "Global", "CommandHistory", self.CommandHistory);
// Keep focus with the edit box
return true;
}
function OnKeyPress(self, evt)
{
evt = DOM.Event.Get(evt);
if (evt.keyCode == Keyboard.Codes.UP)
{
if (self.CommandHistory.length > 0)
{
// Cycle backwards through the command history
self.CommandIndex--;
if (self.CommandIndex < 0)
self.CommandIndex = self.CommandHistory.length - 1;
var command = self.CommandHistory[self.CommandIndex];
self.UserInput.SetValue(command);
}
// Stops default behaviour of moving cursor to the beginning
DOM.Event.StopDefaultAction(evt);
}
else if (evt.keyCode == Keyboard.Codes.DOWN)
{
if (self.CommandHistory.length > 0)
{
// Cycle fowards through the command history
self.CommandIndex = (self.CommandIndex + 1) % self.CommandHistory.length;
var command = self.CommandHistory[self.CommandIndex];
self.UserInput.SetValue(command);
}
// Stops default behaviour of moving cursor to the end
DOM.Event.StopDefaultAction(evt);
}
}
function OnFocus(self)
{
// Reset command index on focus
self.CommandIndex = self.CommandHistory.length;
}
return Console;
})();

View file

@ -1,94 +0,0 @@
//
// Simple wrapper around DataView that auto-advances the read offset and provides
// a few common data type conversions specific to this app
//
DataViewReader = (function ()
{
function DataViewReader(data_view, offset)
{
this.DataView = data_view;
this.Offset = offset;
}
DataViewReader.prototype.AtEnd = function()
{
return this.Offset >= this.DataView.byteLength;
}
DataViewReader.prototype.GetBool = function ()
{
let v = this.DataView.getUint8(this.Offset);
this.Offset++;
return v;
}
DataViewReader.prototype.GetUInt8 = function ()
{
let v = this.DataView.getUint8(this.Offset);
this.Offset++;
return v;
}
DataViewReader.prototype.GetInt32 = function ()
{
let v = this.DataView.getInt32(this.Offset, true);
this.Offset += 4;
return v;
}
DataViewReader.prototype.GetUInt32 = function ()
{
var v = this.DataView.getUint32(this.Offset, true);
this.Offset += 4;
return v;
}
DataViewReader.prototype.GetFloat32 = function ()
{
const v = this.DataView.getFloat32(this.Offset, true);
this.Offset += 4;
return v;
}
DataViewReader.prototype.GetInt64 = function ()
{
var v = this.DataView.getFloat64(this.Offset, true);
this.Offset += 8;
return v;
}
DataViewReader.prototype.GetUInt64 = function ()
{
var v = this.DataView.getFloat64(this.Offset, true);
this.Offset += 8;
return v;
}
DataViewReader.prototype.GetFloat64 = function ()
{
var v = this.DataView.getFloat64(this.Offset, true);
this.Offset += 8;
return v;
}
DataViewReader.prototype.GetStringOfLength = function (string_length)
{
var string = "";
for (var i = 0; i < string_length; i++)
{
string += String.fromCharCode(this.DataView.getInt8(this.Offset));
this.Offset++;
}
return string;
}
DataViewReader.prototype.GetString = function ()
{
var string_length = this.GetUInt32();
return this.GetStringOfLength(string_length);
}
return DataViewReader;
})();

View file

@ -1,123 +0,0 @@
class GLCanvas
{
constructor(width, height)
{
this.width = width;
this.height = height;
// Create a WebGL 2 canvas without premultiplied alpha
this.glCanvas = document.createElement("canvas");
this.glCanvas.width = width;
this.glCanvas.height = height;
this.gl = this.glCanvas.getContext("webgl2", { premultipliedAlpha: false, antialias: false });
// Overlay the canvas on top of everything and make sure mouse events click-through
this.glCanvas.style.position = "fixed";
this.glCanvas.style.pointerEvents = "none";
this.glCanvas.style.zIndex = 1000;
document.body.appendChild(this.glCanvas);
// Hook up resize event handler
DOM.Event.AddHandler(window, "resize", () => this.OnResizeWindow());
this.OnResizeWindow();
// Compile needed shaders
this.timelineProgram = glCreateProgramFromSource(this.gl, "TimelineVShader", TimelineVShader, "TimelineFShader", TimelineFShader);
this.timelineHighlightProgram = glCreateProgramFromSource(this.gl, "TimelineHighlightVShader", TimelineHighlightVShader, "TimelineHighlightFShader", TimelineHighlightFShader);
this.timelineGpuToCpuProgram = glCreateProgramFromSource(this.gl, "TimelineGpuToCpuVShader", TimelineGpuToCpuVShader, "TimelineGpuToCpuFShader", TimelineGpuToCpuFShader);
this.timelineBackgroundProgram = glCreateProgramFromSource(this.gl, "TimelineBackgroundVShader", TimelineBackgroundVShader, "TimelineBackgroundFShader", TimelineBackgroundFShader);
this.gridProgram = glCreateProgramFromSource(this.gl, "GridVShader", GridVShader, "GridFShader", GridFShader);
this.gridNumberProgram = glCreateProgramFromSource(this.gl, "GridNumberVShader", GridNumberVShader, "GridNumberFShader", GridNumberFShader);
this.windowProgram = glCreateProgramFromSource(this.gl, "WindowVShader", WindowVShader, "WindowFShader", WindowFShader);
// Create the shader font resources
this.font = new glFont(this.gl);
this.textBuffer = new glTextBuffer(this.gl, this.font);
this.nameMap = new NameMap(this.textBuffer);
// Kick off the rendering refresh
this.OnDrawHandler = null;
this.Draw(performance.now());
}
SetOnDraw(handler)
{
this.OnDrawHandler = handler;
}
ClearTextResources()
{
this.nameMap = new NameMap(this.textBuffer);
}
OnResizeWindow()
{
// Resize to match the window
this.width = window.innerWidth;
this.height = window.innerHeight;
this.glCanvas.width = window.innerWidth;
this.glCanvas.height = window.innerHeight;
}
SetFontUniforms(program)
{
// Font texture may not be loaded yet
if (this.font.atlasTexture != null)
{
const gl = this.gl;
glSetUniform(gl, program, "inFontAtlasTexture", this.font.atlasTexture, 0);
glSetUniform(gl, program, "inTextBufferDesc.fontWidth", this.font.fontWidth);
glSetUniform(gl, program, "inTextBufferDesc.fontHeight", this.font.fontHeight);
}
}
SetTextUniforms(program)
{
const gl = this.gl;
this.SetFontUniforms(program);
this.textBuffer.SetAsUniform(gl, program, "inTextBuffer", 1);
}
SetContainerUniforms(program, container)
{
const gl = this.gl;
const container_rect = container.getBoundingClientRect();
glSetUniform(gl, program, "inContainer.x0", container_rect.left);
glSetUniform(gl, program, "inContainer.y0", container_rect.top);
glSetUniform(gl, program, "inContainer.x1", container_rect.left + container_rect.width);
glSetUniform(gl, program, "inContainer.y1", container_rect.top + container_rect.height);
}
EnableBlendPremulAlpha()
{
const gl = this.gl;
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
}
DisableBlend()
{
const gl = this.gl;
gl.disable(gl.BLEND);
}
Draw(timestamp)
{
// Setup the viewport and clear the screen
const gl = this.gl;
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
// Chain to the Draw handler
const seconds = timestamp / 1000.0;
if (this.OnDrawHandler != null)
{
this.OnDrawHandler(gl, seconds);
}
// Reschedule
window.requestAnimationFrame((timestamp) => this.Draw(timestamp));
}
};

View file

@ -1,291 +0,0 @@
class GridConfigSamples
{
constructor()
{
this.nbFloatsPerSample = g_nbFloatsPerSample;
this.columns = [];
this.columns.push(new GridColumn("Sample Name", 196));
this.columns.push(new GridColumn("Time (ms)", 56, g_sampleOffsetFloats_Length, 4));
this.columns.push(new GridColumn("Self (ms)", 56, g_sampleOffsetFloats_Self, 4));
this.columns.push(new GridColumn("Calls", 34, g_sampleOffsetFloats_Calls, 0));
this.columns.push(new GridColumn("Depth", 34, g_sampleOffsetFloats_Recurse, 0));
}
}
class GridConfigProperties
{
constructor()
{
this.nbFloatsPerSample = 10;
this.columns = [];
this.columns.push(new GridColumn("Property Name", 196));
this.columns.push(new GridColumn("Value", 90, 4, 4));
this.columns.push(new GridColumn("Prev Value", 90, 6, 4));
}
}
class GridColumn
{
static ColumnTemplate = `<div class="GridNameHeader"></div>`;
constructor(name, width, number_offset, nb_float_chars)
{
// Description
this.name = name;
this.width = width;
this.numberOffset = number_offset;
this.nbFloatChars = nb_float_chars;
// Constants
this.rowHeight = 15;
}
Attach(parent_node)
{
// Generate HTML for the header and parent it
const column = DOM.Node.CreateHTML(GridColumn.ColumnTemplate);
column.innerHTML = this.name;
column.style.width = (this.width - 4) + "px";
this.headerNode = parent_node.appendChild(column);
}
Draw(gl_canvas, x, buffer, scroll_pos, clip, nb_entries, nb_floats_per_sample)
{
// If a number offset in the data stream is provided, we're rendering numbers and not names
if (this.numberOffset !== undefined)
{
this._DrawNumbers(gl_canvas, x, buffer, scroll_pos, clip, nb_entries, nb_floats_per_sample);
}
else
{
this._DrawNames(gl_canvas, x, buffer, scroll_pos, clip, nb_entries, nb_floats_per_sample);
}
}
_DrawNames(gl_canvas, x, buffer, scroll_pos, clip, nb_entries, nb_floats_per_sample)
{
const gl = gl_canvas.gl;
const program = gl_canvas.gridProgram;
gl.useProgram(program);
gl_canvas.SetTextUniforms(program);
this._DrawAny(gl, program, x, buffer, scroll_pos, clip, nb_entries, nb_floats_per_sample);
}
_DrawNumbers(gl_canvas, x, buffer, scroll_pos, clip, nb_entries, nb_floats_per_sample)
{
const gl = gl_canvas.gl;
const program = gl_canvas.gridNumberProgram;
gl.useProgram(program);
gl_canvas.SetFontUniforms(program);
glSetUniform(gl, program, "inNumberOffset", this.numberOffset);
glSetUniform(gl, program, "inNbFloatChars", this.nbFloatChars);
this._DrawAny(gl, program, x, buffer, scroll_pos, clip, nb_entries, nb_floats_per_sample);
}
_DrawAny(gl, program, x, buffer, scroll_pos, clip, nb_entries, nb_floats_per_sample)
{
const clip_min_x = clip[0];
const clip_min_y = clip[1];
const clip_max_x = clip[2];
const clip_max_y = clip[3];
// Scrolled position of the grid
const pos_x = clip_min_x + scroll_pos[0] + x;
const pos_y = clip_min_y + scroll_pos[1];
// Clip column to the window
const min_x = Math.min(Math.max(clip_min_x, pos_x), clip_max_x);
const min_y = Math.min(Math.max(clip_min_y, pos_y), clip_max_y);
const max_x = Math.max(Math.min(clip_max_x, pos_x + this.width), clip_min_x);
const max_y = Math.max(Math.min(clip_max_y, pos_y + nb_entries * this.rowHeight), clip_min_y);
// Don't render if outside the bounds of the main window
if (min_x > gl.canvas.width || max_x < 0 || min_y > gl.canvas.height || max_y < 0)
{
return;
}
const pixel_offset_x = Math.max(min_x - pos_x, 0);
const pixel_offset_y = Math.max(min_y - pos_y, 0);
// Viewport constants
glSetUniform(gl, program, "inViewport.width", gl.canvas.width);
glSetUniform(gl, program, "inViewport.height", gl.canvas.height);
// Grid constants
glSetUniform(gl, program, "inGrid.minX", min_x);
glSetUniform(gl, program, "inGrid.minY", min_y);
glSetUniform(gl, program, "inGrid.maxX", max_x);
glSetUniform(gl, program, "inGrid.maxY", max_y);
glSetUniform(gl, program, "inGrid.pixelOffsetX", pixel_offset_x);
glSetUniform(gl, program, "inGrid.pixelOffsetY", pixel_offset_y);
// Source data set buffers
glSetUniform(gl, program, "inSamples", buffer.texture, 2);
glSetUniform(gl, program, "inSamplesLength", buffer.nbEntries);
glSetUniform(gl, program, "inFloatsPerSample", nb_floats_per_sample);
glSetUniform(gl, program, "inNbSamples", buffer.nbEntries / nb_floats_per_sample);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
}
class GridWindow
{
static GridTemplate = `
<div style="height:100%; overflow: hidden; position: relative; display:flex; flex-flow: column;">
<div style="height: 16px; flex: 0 1 auto;"></div>
<div style="flex: 1 1 auto;"></div>
</div>
`;
constructor(wm, name, offset, gl_canvas, config)
{
this.nbEntries = 0;
this.scrollPos = [ 0, 0 ];
// Window setup
this.xPos = 10 + offset * 410;
this.window = wm.AddWindow(name, 100, 100, 100, 100, null, this);
this.window.ShowNoAnim();
this.visible = true;
// Cache how much internal padding the window has, for clipping
const style = getComputedStyle(this.window.BodyNode);
this.bodyPadding = parseFloat(style.padding);
// Create the Grid host HTML
const grid_node = DOM.Node.CreateHTML(GridWindow.GridTemplate);
this.gridNode = this.window.BodyNode.appendChild(grid_node);
this.headerNode = this.gridNode.children[0];
this.contentNode = this.gridNode.children[1];
// Build column data
this.nbFloatsPerSample = config.nbFloatsPerSample;
this.columns = config.columns;
for (let column of this.columns)
{
column.Attach(this.headerNode);
}
this._PositionHeaders();
// Header nodes have 1 pixel borders so the first column is required to have a width 1 less than everything else
// To counter that, shift the first header node one to the left (it will clip) so that it can have its full width
this.columns[0].headerNode.style.marginLeft = "-1px";
// Setup for pan/wheel scrolling
this.mouseInteraction = new MouseInteraction(this.window.BodyNode);
this.mouseInteraction.onMoveHandler = (mouse_state, mx, my) => this._OnMouseMove(mouse_state, mx, my);
this.mouseInteraction.onScrollHandler = (mouse_state) => this._OnMouseScroll(mouse_state);
const gl = gl_canvas.gl;
this.glCanvas = gl_canvas;
this.sampleBuffer = new glDynamicBuffer(gl, glDynamicBufferType.Texture, gl.FLOAT, 1);
}
Close()
{
this.window.Close();
}
static AnimatedMove(self, top_window, bottom_window, val)
{
self.xPos = val;
self.WindowResized(top_window, bottom_window);
}
SetXPos(xpos, top_window, bottom_window)
{
Anim.Animate(
Bind(GridWindow.AnimatedMove, this, top_window, bottom_window),
this.xPos, 10 + xpos * 410, 0.25);
}
SetVisible(visible)
{
if (visible != this.visible)
{
if (visible == true)
this.window.ShowNoAnim();
else
this.window.HideNoAnim();
this.visible = visible;
}
}
WindowResized(top_window, bottom_window)
{
const top = top_window.Position[1] + top_window.Size[1] + 10;
this.window.SetPosition(this.xPos, top_window.Position[1] + top_window.Size[1] + 10);
this.window.SetSize(400, bottom_window.Position[1] - 10 - top);
}
UpdateEntries(nb_entries, samples)
{
// This tracks the latest actual entry count
this.nbEntries = nb_entries;
// Resize buffers to match any new entry count
if (nb_entries * this.nbFloatsPerSample > this.sampleBuffer.nbEntries)
{
this.sampleBuffer.ResizeToFitNextPow2(nb_entries * this.nbFloatsPerSample);
}
// Copy and upload the entry data
this.sampleBuffer.cpuArray.set(samples);
this.sampleBuffer.UploadData();
}
Draw()
{
// Establish content node clipping rectangle
const rect = this.contentNode.getBoundingClientRect();
const clip = [
rect.left,
rect.top,
rect.left + rect.width,
rect.top + rect.height,
];
// Draw columns, left-to-right
let x = 0;
for (let column of this.columns)
{
column.Draw(this.glCanvas, x, this.sampleBuffer, this.scrollPos, clip, this.nbEntries, this.nbFloatsPerSample);
x += column.width + 1;
}
}
_PositionHeaders()
{
let x = this.scrollPos[0];
for (let i in this.columns)
{
const column = this.columns[i];
column.headerNode.style.left = x + "px";
x += column.width;
x += (i >= 1) ? 1 : 0;
}
}
_OnMouseMove(mouse_state, mouse_offset_x, mouse_offset_y)
{
this.scrollPos[0] = Math.min(0, this.scrollPos[0] + mouse_offset_x);
this.scrollPos[1] = Math.min(0, this.scrollPos[1] + mouse_offset_y);
this._PositionHeaders();
}
_OnMouseScroll(mouse_state)
{
this.scrollPos[1] = Math.min(0, this.scrollPos[1] + mouse_state.WheelDelta * 15);
}
}

View file

@ -1,106 +0,0 @@
class MouseInteraction
{
constructor(node)
{
this.node = node;
// Current interaction state
this.mouseDown = false;
this.lastMouseState = null;
this.mouseMoved = false;
// Empty user handlers
this.onClickHandler = null;
this.onMoveHandler = null;
this.onHoverHandler = null;
this.onScrollHandler = null;
// Setup DOM handlers
DOM.Event.AddHandler(this.node, "mousedown", (evt) => this._OnMouseDown(evt));
DOM.Event.AddHandler(this.node, "mouseup", (evt) => this._OnMouseUp(evt));
DOM.Event.AddHandler(this.node, "mousemove", (evt) => this._OnMouseMove(evt));
DOM.Event.AddHandler(this.node, "mouseleave", (evt) => this._OnMouseLeave(evt));
// Mouse wheel is a little trickier
const mouse_wheel_event = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel";
DOM.Event.AddHandler(this.node, mouse_wheel_event, (evt) => this._OnMouseScroll(evt));
}
_OnMouseDown(evt)
{
this.mouseDown = true;
this.lastMouseState = new Mouse.State(evt);
this.mouseMoved = false;
DOM.Event.StopDefaultAction(evt);
}
_OnMouseUp(evt)
{
const mouse_state = new Mouse.State(evt);
this.mouseDown = false;
// Chain to on click event when released without movement
if (!this.mouseMoved)
{
if (this.onClickHandler)
{
this.onClickHandler(mouse_state);
}
}
}
_OnMouseMove(evt)
{
const mouse_state = new Mouse.State(evt);
if (this.mouseDown)
{
// Has the mouse moved while being held down?
const move_offset_x = mouse_state.Position[0] - this.lastMouseState.Position[0];
const move_offset_y = mouse_state.Position[1] - this.lastMouseState.Position[1];
if (move_offset_x != 0 || move_offset_y != 0)
{
this.mouseMoved = true;
// Chain to move handler
if (this.onMoveHandler)
{
this.onMoveHandler(mouse_state, move_offset_x, move_offset_y);
}
}
}
// Chain to hover handler
else if (this.onHoverHandler)
{
this.onHoverHandler(mouse_state);
}
this.lastMouseState = mouse_state;
}
_OnMouseLeave(evt)
{
// Cancel panning
this.mouseDown = false;
// Cancel hovering
if (this.onHoverHandler)
{
this.onHoverHandler(null);
}
}
_OnMouseScroll(evt)
{
const mouse_state = new Mouse.State(evt);
if (this.onScrollHandler)
{
this.onScrollHandler(mouse_state);
// Prevent vertical scrolling on mouse-wheel
DOM.Event.StopDefaultAction(evt);
}
}
};

View file

@ -1,53 +0,0 @@
class NameMap
{
constructor(text_buffer)
{
this.names = { };
this.textBuffer = text_buffer;
}
Get(name_hash)
{
// Return immediately if it's in the hash
let name = this.names[name_hash];
if (name != undefined)
{
return [ true, name ];
}
// Create a temporary name that uses the hash
name = {
string: name_hash.toString(),
hash: name_hash
};
this.names[name_hash] = name;
// Add to the text buffer the first time this name is encountered
name.textEntry = this.textBuffer.AddText(name.string);
return [ false, name ];
}
Set(name_hash, name_string)
{
// Create the name on-demand if its hash doesn't exist
let name = this.names[name_hash];
if (name == undefined)
{
name = {
string: name_string,
hash: name_hash
};
this.names[name_hash] = name;
}
else
{
name.string = name_string;
}
// Apply the updated text to the buffer
name.textEntry = this.textBuffer.AddText(name_string);
return name;
}
}

View file

@ -1,61 +0,0 @@
class PixelTimeRange
{
constructor(start_us, span_us, span_px)
{
this.Span_px = span_px;
this.Set(start_us, span_us);
}
Set(start_us, span_us)
{
this.Start_us = start_us;
this.Span_us = span_us;
this.End_us = this.Start_us + span_us;
this.usPerPixel = this.Span_px / this.Span_us;
}
SetStart(start_us)
{
this.Start_us = start_us;
this.End_us = start_us + this.Span_us;
}
SetEnd(end_us)
{
this.End_us = end_us;
this.Start_us = end_us - this.Span_us;
}
SetPixelSpan(span_px)
{
this.Span_px = span_px;
this.usPerPixel = this.Span_px / this.Span_us;
}
PixelOffset(time_us)
{
return Math.floor((time_us - this.Start_us) * this.usPerPixel);
}
PixelSize(time_us)
{
return Math.floor(time_us * this.usPerPixel);
}
TimeAtPosition(position)
{
return this.Start_us + position / this.usPerPixel;
}
Clone()
{
return new PixelTimeRange(this.Start_us, this.Span_us, this.Span_px);
}
SetAsUniform(gl, program)
{
glSetUniform(gl, program, "inTimeRange.msStart", this.Start_us / 1000.0);
glSetUniform(gl, program, "inTimeRange.msPerPixel", this.usPerPixel * 1000.0);
}
}

View file

@ -1,756 +0,0 @@
//
// TODO: Window resizing needs finer-grain control
// TODO: Take into account where user has moved the windows
// TODO: Controls need automatic resizing within their parent windows
//
Settings = (function()
{
function Settings()
{
this.IsPaused = false;
this.SyncTimelines = true;
}
return Settings;
})();
Remotery = (function()
{
// crack the url and get the parameter we want
var getUrlParameter = function getUrlParameter( search_param)
{
var page_url = decodeURIComponent( window.location.search.substring(1) ),
url_vars = page_url.split('&'),
param_name,
i;
for (i = 0; i < url_vars.length; i++)
{
param_name = url_vars[i].split('=');
if (param_name[0] === search_param)
{
return param_name[1] === undefined ? true : param_name[1];
}
}
};
function Remotery()
{
this.WindowManager = new WM.WindowManager();
this.Settings = new Settings();
// "addr" param is ip:port and will override the local store version if passed in the URL
var addr = getUrlParameter( "addr" );
if ( addr != null )
this.ConnectionAddress = "ws://" + addr + "/rmt";
else
this.ConnectionAddress = LocalStore.Get("App", "Global", "ConnectionAddress", "ws://127.0.0.1:17815/rmt");
this.Server = new WebSocketConnection();
this.Server.AddConnectHandler(Bind(OnConnect, this));
this.Server.AddDisconnectHandler(Bind(OnDisconnect, this));
this.glCanvas = new GLCanvas(100, 100);
this.glCanvas.SetOnDraw((gl, seconds) => this.OnGLCanvasDraw(gl, seconds));
// Create the console up front as everything reports to it
this.Console = new Console(this.WindowManager, this.Server);
// Create required windows
this.TitleWindow = new TitleWindow(this.WindowManager, this.Settings, this.Server, this.ConnectionAddress);
this.TitleWindow.SetConnectionAddressChanged(Bind(OnAddressChanged, this));
this.SampleTimelineWindow = new TimelineWindow(this.WindowManager, "Sample Timeline", this.Settings, Bind(OnTimelineCheck, this), this.glCanvas);
this.SampleTimelineWindow.SetOnHover(Bind(OnSampleHover, this));
this.SampleTimelineWindow.SetOnSelected(Bind(OnSampleSelected, this));
this.ProcessorTimelineWindow = new TimelineWindow(this.WindowManager, "Processor Timeline", this.Settings, null, this.glCanvas);
this.SampleTimelineWindow.SetOnMoved(Bind(OnTimelineMoved, this));
this.ProcessorTimelineWindow.SetOnMoved(Bind(OnTimelineMoved, this));
this.TraceDrop = new TraceDrop(this);
this.nbGridWindows = 0;
this.gridWindows = { };
this.FrameHistory = { };
this.ProcessorFrameHistory = { };
this.PropertyFrameHistory = [ ];
this.SelectedFrames = { };
this.Server.AddMessageHandler("SMPL", Bind(OnSamples, this));
this.Server.AddMessageHandler("SSMP", Bind(OnSampleName, this));
this.Server.AddMessageHandler("PRTH", Bind(OnProcessorThreads, this));
this.Server.AddMessageHandler("PSNP", Bind(OnPropertySnapshots, this));
// Kick-off the auto-connect loop
AutoConnect(this);
// Hook up resize event handler
DOM.Event.AddHandler(window, "resize", Bind(OnResizeWindow, this));
OnResizeWindow(this);
}
Remotery.prototype.Clear = function()
{
// Clear timelines
this.SampleTimelineWindow.Clear();
this.ProcessorTimelineWindow.Clear();
// Close and clear all sample windows
for (var i in this.gridWindows)
{
const grid_window = this.gridWindows[i];
grid_window.Close();
}
this.nbGridWindows = 0;
this.gridWindows = { };
this.propertyGridWindow = this.AddGridWindow("__rmt__global__properties__", "Global Properties", new GridConfigProperties());
// Clear runtime data
this.FrameHistory = { };
this.ProcessorFrameHistory = { };
this.PropertyFrameHistory = [ ];
this.SelectedFrames = { };
this.glCanvas.ClearTextResources();
// Resize everything to fit new layout
OnResizeWindow(this);
}
function DrawWindowMask(gl, program, window_node)
{
gl.useProgram(program);
// Using depth as a mask
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
// Viewport constants
glSetUniform(gl, program, "inViewport.width", gl.canvas.width);
glSetUniform(gl, program, "inViewport.height", gl.canvas.height);
// Window dimensions
const rect = window_node.getBoundingClientRect();
glSetUniform(gl, program, "minX", rect.left);
glSetUniform(gl, program, "minY", rect.top);
glSetUniform(gl, program, "maxX", rect.left + rect.width);
glSetUniform(gl, program, "maxY", rect.top + rect.height);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
Remotery.prototype.OnGLCanvasDraw = function(gl, seconds)
{
this.glCanvas.textBuffer.UploadData();
// Draw windows in their z-order, front-to-back
// Depth test is enabled, rejecting equal z, and windows render transparent
// Draw window content first, then draw the invisible window mask
// Any windows that come after another window will not draw where the previous window already masked out depth
for (let window of this.WindowManager.Windows)
{
// Some windows might not have WebGL drawing on them; they need to occlude as well
DrawWindowMask(gl, this.glCanvas.windowProgram, window.Node);
if (window.userData != null)
{
window.userData.Draw();
}
}
}
function AutoConnect(self)
{
// Only attempt to connect if there isn't already a connection or an attempt to connect
if (!self.Server.Connected() && !self.Server.Connecting())
{
self.Server.Connect(self.ConnectionAddress);
}
// Always schedule another check
window.setTimeout(Bind(AutoConnect, self), 2000);
}
function OnConnect(self)
{
// Connection address has been validated
LocalStore.Set("App", "Global", "ConnectionAddress", self.ConnectionAddress);
self.Clear();
// Ensure the viewer is ready for realtime updates
self.TitleWindow.Unpause();
}
function OnDisconnect(self)
{
// Pause so the user can inspect the trace
self.TitleWindow.Pause();
}
function OnAddressChanged(self, node)
{
// Update and disconnect, relying on auto-connect to reconnect
self.ConnectionAddress = node.value;
self.Server.Disconnect();
// Give input focus away
return false;
}
Remotery.prototype.AddGridWindow = function(name, display_name, config)
{
const grid_window = new GridWindow(this.WindowManager, display_name, this.nbGridWindows, this.glCanvas, config);
this.gridWindows[name] = grid_window;
this.gridWindows[name].WindowResized(this.SampleTimelineWindow.Window, this.Console.Window);
this.nbGridWindows++;
MoveGridWindows(this);
return grid_window;
}
function DecodeSampleHeader(self, data_view_reader, length)
{
// Message-specific header
let message = { };
message.messageStart = data_view_reader.Offset;
message.thread_name = data_view_reader.GetString();
message.nb_samples = data_view_reader.GetUInt32();
message.partial_tree = data_view_reader.GetUInt32();
// Align sample reading to 32-bit boundary
const align = ((4 - (data_view_reader.Offset & 3)) & 3);
data_view_reader.Offset += align;
message.samplesStart = data_view_reader.Offset;
message.samplesLength = length - (message.samplesStart - message.messageStart);
return message;
}
function SetNanosecondsAsMilliseconds(samples_view, offset)
{
samples_view.setFloat32(offset, samples_view.getFloat64(offset, true) / 1000.0, true);
}
function SetUint32AsFloat32(samples_view, offset)
{
samples_view.setFloat32(offset, samples_view.getUint32(offset, true), true);
}
function ProcessSampleTree(self, sample_data, message)
{
const empty_text_entry = {
offset: 0,
length: 1,
};
const samples_length = message.nb_samples * g_nbBytesPerSample;
const samples_view = new DataView(sample_data, message.samplesStart, samples_length);
message.sampleDataView = samples_view;
for (let offset = 0; offset < samples_length; offset += g_nbBytesPerSample)
{
// Get name hash and lookup in name map
const name_hash = samples_view.getUint32(offset, true);
const [ name_exists, name ] = self.glCanvas.nameMap.Get(name_hash);
// If the name doesn't exist in the map yet, request it from the server
if (!name_exists)
{
if (self.Server.Connected())
{
self.Server.Send("GSMP" + name_hash);
}
}
// Add sample name text buffer location
const text_entry = name.textEntry != null ? name.textEntry : empty_text_entry;
samples_view.setFloat32(offset + g_sampleOffsetBytes_NameOffset, text_entry.offset, true);
samples_view.setFloat32(offset + g_sampleOffsetBytes_NameLength, text_entry.length, true);
// Time in milliseconds
SetNanosecondsAsMilliseconds(samples_view, offset + g_sampleOffsetBytes_Start);
SetNanosecondsAsMilliseconds(samples_view, offset + g_sampleOffsetBytes_Length);
SetNanosecondsAsMilliseconds(samples_view, offset + g_sampleOffsetBytes_Self);
SetNanosecondsAsMilliseconds(samples_view, offset + g_sampleOffsetBytes_GpuToCpu);
// Convert call count/recursion integers to float
SetUint32AsFloat32(samples_view, offset + g_sampleOffsetBytes_Calls);
SetUint32AsFloat32(samples_view, offset + g_sampleOffsetBytes_Recurse);
}
// Convert to floats for GPU
message.sampleFloats = new Float32Array(sample_data, message.samplesStart, message.nb_samples * g_nbFloatsPerSample);
}
function OnSamples(self, socket, data_view_reader, length)
{
// Discard any new samples while paused and connected
// Otherwise this stops a paused Remotery from loading new samples from disk
if (self.Settings.IsPaused && self.Server.Connected())
return;
// Binary decode incoming sample data
var message = DecodeSampleHeader(self, data_view_reader, length);
if (message.nb_samples == 0)
{
return;
}
var name = message.thread_name;
ProcessSampleTree(self, data_view_reader.DataView.buffer, message);
// Add to frame history for this thread
var thread_frame = new ThreadFrame(message);
if (!(name in self.FrameHistory))
{
self.FrameHistory[name] = [ ];
}
var frame_history = self.FrameHistory[name];
if (frame_history.length > 0 && frame_history[frame_history.length - 1].PartialTree)
{
// Always overwrite partial trees with new information
frame_history[frame_history.length - 1] = thread_frame;
}
else
{
frame_history.push(thread_frame);
}
// Discard old frames to keep memory-use constant
var max_nb_frames = 10000;
var extra_frames = frame_history.length - max_nb_frames;
if (extra_frames > 0)
frame_history.splice(0, extra_frames);
// Create sample windows on-demand
if (!(name in self.gridWindows))
{
self.AddGridWindow(name, name, new GridConfigSamples());
}
// Set on the window and timeline if connected as this implies a trace is being loaded, which we want to speed up
if (self.Server.Connected())
{
self.gridWindows[name].UpdateEntries(message.nb_samples, message.sampleFloats);
self.SampleTimelineWindow.OnSamples(name, frame_history);
}
}
function OnSampleName(self, socket, data_view_reader)
{
// Add any names sent by the server to the local map
let name_hash = data_view_reader.GetUInt32();
let name_string = data_view_reader.GetString();
self.glCanvas.nameMap.Set(name_hash, name_string);
}
function OnProcessorThreads(self, socket, data_view_reader)
{
// Discard any new samples while paused and connected
// Otherwise this stops a paused Remotery from loading new samples from disk
if (self.Settings.IsPaused && self.Server.Connected())
return;
let nb_processors = data_view_reader.GetUInt32();
let message_index = data_view_reader.GetUInt64();
const empty_text_entry = {
offset: 0,
length: 1,
};
// Decode each processor
for (let i = 0; i < nb_processors; i++)
{
let thread_id = data_view_reader.GetUInt32();
let thread_name_hash = data_view_reader.GetUInt32();
let sample_time = data_view_reader.GetUInt64();
// Add frame history for this processor
let processor_name = "Processor " + i.toString();
if (!(processor_name in self.ProcessorFrameHistory))
{
self.ProcessorFrameHistory[processor_name] = [ ];
}
let frame_history = self.ProcessorFrameHistory[processor_name];
if (thread_id == 0xFFFFFFFF)
{
continue;
}
// Try to merge this frame's samples with the previous frame if the are the same thread
if (frame_history.length > 0)
{
let last_thread_frame = frame_history[frame_history.length - 1];
if (last_thread_frame.threadId == thread_id && last_thread_frame.messageIndex == message_index - 1)
{
// Update last frame message index so that the next frame can check for continuity
last_thread_frame.messageIndex = message_index;
// Sum time elapsed on the previous frame
const us_length = sample_time - last_thread_frame.usLastStart;
last_thread_frame.usLastStart = sample_time;
last_thread_frame.EndTime_us += us_length;
const last_length = last_thread_frame.sampleDataView.getFloat32(g_sampleOffsetBytes_Length, true);
last_thread_frame.sampleDataView.setFloat32(g_sampleOffsetBytes_Length, last_length + us_length / 1000.0, true);
continue;
}
}
// Discard old frames to keep memory-use constant
var max_nb_frames = 10000;
var extra_frames = frame_history.length - max_nb_frames;
if (extra_frames > 0)
{
frame_history.splice(0, extra_frames);
}
// Lookup the thread name
let [ name_exists, thread_name ] = self.glCanvas.nameMap.Get(thread_name_hash);
// If the name doesn't exist in the map yet, request it from the server
if (!name_exists)
{
if (self.Server.Connected())
{
self.Server.Send("GSMP" + thread_name_hash);
}
}
// We are co-opting the sample rendering functionality of the timeline window to display processor threads as
// thread samples. Fabricate a thread frame message, packing the processor info into one root sample.
// TODO(don): Abstract the timeline window for pure range display as this is quite inefficient.
let thread_message = { };
thread_message.nb_samples = 1;
thread_message.sampleData = new ArrayBuffer(g_nbBytesPerSample);
thread_message.sampleDataView = new DataView(thread_message.sampleData);
const sample_data_view = thread_message.sampleDataView;
// Set the name
const text_entry = thread_name.textEntry != null ? thread_name.textEntry : empty_text_entry;
sample_data_view.setFloat32(g_sampleOffsetBytes_NameOffset, text_entry.offset, true);
sample_data_view.setFloat32(g_sampleOffsetBytes_NameLength, text_entry.length, true);
// Make a pastel-y colour from the thread name hash
const hash = thread_name.hash;
sample_data_view.setUint8(g_sampleOffsetBytes_Colour + 0, 127 + (hash & 255) / 2);
sample_data_view.setUint8(g_sampleOffsetBytes_Colour + 1, 127 + ((hash >> 4) & 255) / 2);
sample_data_view.setUint8(g_sampleOffsetBytes_Colour + 2, 127 + ((hash >> 8) & 255) / 2);
// Set the time
sample_data_view.setFloat32(g_sampleOffsetBytes_Start, sample_time / 1000.0, true);
sample_data_view.setFloat32(g_sampleOffsetBytes_Length, 0.25, true);
thread_message.sampleFloats = new Float32Array(thread_message.sampleData, 0, thread_message.nb_samples * g_nbFloatsPerSample);
// Create a thread frame and annotate with data required to merge processor samples
let thread_frame = new ThreadFrame(thread_message);
thread_frame.threadId = thread_id;
thread_frame.messageIndex = message_index;
thread_frame.usLastStart = sample_time;
frame_history.push(thread_frame);
if (self.Server.Connected())
{
self.ProcessorTimelineWindow.OnSamples(processor_name, frame_history);
}
}
}
function UInt64ToFloat32(view, offset)
{
// Unpack as two 32-bit integers so we have a vague attempt at reconstructing the value
const a = view.getUint32(offset + 0, true);
const b = view.getUint32(offset + 4, true);
// Can't do bit arithmetic above 32-bits in JS so combine using power-of-two math
const v = a + (b * Math.pow(2, 32));
// TODO(don): Potentially massive data loss!
snapshots_view.setFloat32(offset, v);
}
function SInt64ToFloat32(view, offset)
{
// Unpack as two 32-bit integers so we have a vague attempt at reconstructing the value
const a = view.getUint32(offset + 0, true);
const b = view.getUint32(offset + 4, true);
// Is this negative?
if (b & 0x80000000)
{
// Can only convert from twos-complement with 32-bit arithmetic so shave off the upper 32-bits
// TODO(don): Crazy data loss here
const v = -(~(a - 1));
}
else
{
// Can't do bit arithmetic above 32-bits in JS so combine using power-of-two math
const v = a + (b * Math.pow(2, 32));
}
// TODO(don): Potentially massive data loss!
snapshots_view.setFloat32(offset, v);
}
function DecodeSnapshotHeader(self, data_view_reader, length)
{
// Message-specific header
let message = { };
message.messageStart = data_view_reader.Offset;
message.nbSnapshots = data_view_reader.GetUInt32();
message.propertyFrame = data_view_reader.GetUInt32();
message.snapshotsStart = data_view_reader.Offset;
message.snapshotsLength = length - (message.snapshotsStart - message.messageStart);
return message;
}
function ProcessSnapshots(self, snapshot_data, message)
{
if (self.Settings.IsPaused)
{
return null;
}
const empty_text_entry = {
offset: 0,
length: 1,
};
const snapshots_length = message.nbSnapshots * g_nbBytesPerSnapshot;
const snapshots_view = new DataView(snapshot_data, message.snapshotsStart, snapshots_length);
for (let offset = 0; offset < snapshots_length; offset += g_nbBytesPerSnapshot)
{
// Get name hash and lookup in name map
const name_hash = snapshots_view.getUint32(offset, true);
const [ name_exists, name ] = self.glCanvas.nameMap.Get(name_hash);
// If the name doesn't exist in the map yet, request it from the server
if (!name_exists)
{
if (self.Server.Connected())
{
self.Server.Send("GSMP" + name_hash);
}
}
// Add snapshot name text buffer location
const text_entry = name.textEntry != null ? name.textEntry : empty_text_entry;
snapshots_view.setFloat32(offset + 0, text_entry.offset, true);
snapshots_view.setFloat32(offset + 4, text_entry.length, true);
// Heat colour style falloff to quickly identify modified properties
let r = 255, g = 255, b = 255;
const prev_value_frame = snapshots_view.getUint32(offset + 32, true);
const frame_delta = message.propertyFrame - prev_value_frame;
if (frame_delta < 64)
{
g = Math.min(Math.min(frame_delta, 32) * 8, 255);
b = Math.min(frame_delta * 4, 255);
}
snapshots_view.setUint8(offset + 8, r);
snapshots_view.setUint8(offset + 9, g);
snapshots_view.setUint8(offset + 10, b);
const snapshot_type = snapshots_view.getUint32(offset + 12, true);
switch (snapshot_type)
{
case 1:
case 2:
case 3:
case 4:
case 7:
snapshots_view.setFloat32(offset + 16, snapshots_view.getFloat64(offset + 16, true), true);
snapshots_view.setFloat32(offset + 24, snapshots_view.getFloat64(offset + 24, true), true);
break;
// Unpack 64-bit integers stored full precision in the logs and view them to the best of our current abilities
case 5:
SInt64ToFloat32(snapshots_view, offset + 16);
SInt64ToFloat32(snapshots_view, offset + 24);
case 6:
UInt64ToFloat32(snapshots_view, offset + 16);
UInt64ToFloat32(snapshots_view, offset + 24);
break;
}
}
// Convert to floats for GPU
return new Float32Array(snapshot_data, message.snapshotsStart, message.nbSnapshots * g_nbFloatsPerSnapshot);
}
function OnPropertySnapshots(self, socket, data_view_reader, length)
{
// Discard any new snapshots while paused and connected
// Otherwise this stops a paused Remotery from loading new samples from disk
if (self.Settings.IsPaused && self.Server.Connected())
return;
// Binary decode incoming snapshot data
const message = DecodeSnapshotHeader(self, data_view_reader, length);
message.snapshotFloats = ProcessSnapshots(self, data_view_reader.DataView.buffer, message);
// Add to frame history
const thread_frame = new PropertySnapshotFrame(message);
const frame_history = self.PropertyFrameHistory;
frame_history.push(thread_frame);
// Discard old frames to keep memory-use constant
var max_nb_frames = 10000;
var extra_frames = frame_history.length - max_nb_frames;
if (extra_frames > 0)
frame_history.splice(0, extra_frames);
// Set on the window if connected as this implies a trace is being loaded, which we want to speed up
if (self.Server.Connected())
{
self.propertyGridWindow.UpdateEntries(message.nbSnapshots, message.snapshotFloats);
}
}
function OnTimelineCheck(self, name, evt)
{
// Show/hide the equivalent sample window and move all the others to occupy any left-over space
var target = DOM.Event.GetNode(evt);
self.gridWindows[name].SetVisible(target.checked);
MoveGridWindows(self);
}
function MoveGridWindows(self)
{
// Stack all windows next to each other
let xpos = 0;
for (let i in self.gridWindows)
{
const grid_window = self.gridWindows[i];
if (grid_window.visible)
{
grid_window.SetXPos(xpos++, self.SampleTimelineWindow.Window, self.Console.Window);
}
}
}
function OnSampleHover(self, thread_name, hover)
{
if (!self.Settings.IsPaused)
{
return;
}
// Search for the grid window for the thread being hovered over
for (let window_thread_name in self.gridWindows)
{
if (window_thread_name == thread_name)
{
const grid_window = self.gridWindows[thread_name];
// Populate with the sample under hover
if (hover != null)
{
const frame = hover[0];
grid_window.UpdateEntries(frame.NbSamples, frame.sampleFloats);
}
// When there's no hover, go back to the selected frame
else if (self.SelectedFrames[thread_name])
{
const frame = self.SelectedFrames[thread_name];
grid_window.UpdateEntries(frame.NbSamples, frame.sampleFloats);
}
// Otherwise display the last sample in the frame
else
{
const frames = self.FrameHistory[thread_name];
const frame = frames[frames.length - 1];
grid_window.UpdateEntries(frame.NbSamples, frame.sampleFloats);
}
break;
}
}
}
function OnSampleSelected(self, thread_name, select)
{
// Lookup sample window
if (thread_name in self.gridWindows)
{
const grid_window = self.gridWindows[thread_name];
// Set the grid window to the selected frame if valid
if (select)
{
const frame = select[0];
self.SelectedFrames[thread_name] = frame;
grid_window.UpdateEntries(frame.NbSamples, frame.sampleFloats);
}
// Otherwise deselect
else
{
const frames = self.FrameHistory[thread_name];
const frame = frames[frames.length - 1];
self.SelectedFrames[thread_name] = null;
grid_window.UpdateEntries(frame.NbSamples, frame.sampleFloats);
self.SampleTimelineWindow.Deselect(thread_name);
}
}
}
function OnResizeWindow(self)
{
var w = window.innerWidth;
var h = window.innerHeight;
// Resize windows
self.Console.WindowResized(w, h);
self.TitleWindow.WindowResized(w, h);
self.SampleTimelineWindow.WindowResized(10, w / 2 - 5, self.TitleWindow.Window);
self.ProcessorTimelineWindow.WindowResized(w / 2 + 5, w / 2 - 5, self.TitleWindow.Window);
for (var i in self.gridWindows)
{
self.gridWindows[i].WindowResized(self.SampleTimelineWindow.Window, self.Console.Window);
}
}
function OnTimelineMoved(self, timeline)
{
if (self.Settings.SyncTimelines)
{
let other_timeline = timeline == self.ProcessorTimelineWindow ? self.SampleTimelineWindow : self.ProcessorTimelineWindow;
other_timeline.SetTimeRange(timeline.TimeRange.Start_us, timeline.TimeRange.Span_us);
}
}
return Remotery;
})();

View file

@ -1,28 +0,0 @@
// Sample properties when viewed as an array of floats
const g_nbFloatsPerSample = 14;
const g_sampleOffsetFloats_NameOffset = 0;
const g_sampleOffsetFloats_NameLength = 1;
const g_sampleOffsetFloats_Start = 3;
const g_sampleOffsetFloats_Length = 5;
const g_sampleOffsetFloats_Self = 7;
const g_sampleOffsetFloats_GpuToCpu = 9;
const g_sampleOffsetFloats_Calls = 11;
const g_sampleOffsetFloats_Recurse = 12;
// Sample properties when viewed as bytes
const g_nbBytesPerSample = g_nbFloatsPerSample * 4;
const g_sampleOffsetBytes_NameOffset = g_sampleOffsetFloats_NameOffset * 4;
const g_sampleOffsetBytes_NameLength = g_sampleOffsetFloats_NameLength * 4;
const g_sampleOffsetBytes_Colour = 8;
const g_sampleOffsetBytes_Depth = 11;
const g_sampleOffsetBytes_Start = g_sampleOffsetFloats_Start * 4;
const g_sampleOffsetBytes_Length = g_sampleOffsetFloats_Length * 4;
const g_sampleOffsetBytes_Self = g_sampleOffsetFloats_Self * 4;
const g_sampleOffsetBytes_GpuToCpu = g_sampleOffsetFloats_GpuToCpu * 4;
const g_sampleOffsetBytes_Calls = g_sampleOffsetFloats_Calls * 4;
const g_sampleOffsetBytes_Recurse = g_sampleOffsetFloats_Recurse * 4;
// Snapshot properties
const g_nbFloatsPerSnapshot = 10;
const g_nbBytesPerSnapshot = g_nbFloatsPerSnapshot * 4;

View file

@ -1,162 +0,0 @@
const GridShaderShared = ShaderShared + `
#define RowHeight 15.0
struct Grid
{
float minX;
float minY;
float maxX;
float maxY;
float pixelOffsetX;
float pixelOffsetY;
};
uniform Viewport inViewport;
uniform Grid inGrid;
float Row(vec2 pixel_position)
{
return floor(pixel_position.y / RowHeight);
}
vec3 RowColour(float row)
{
float row_grey = (int(row) & 1) == 0 ? 0.25 : 0.23;
return vec3(row_grey);
}
`;
// -------------------------------------------------------------------------------------------------------------------------------
// Vertex Shader
// -------------------------------------------------------------------------------------------------------------------------------
const GridVShader = GridShaderShared + `
out vec2 varPixelPosition;
void main()
{
vec2 position = QuadPosition(gl_VertexID, inGrid.minX, inGrid.minY, inGrid.maxX, inGrid.maxY);
vec4 ndc_pos = UVToNDC(inViewport, position);
gl_Position = ndc_pos;
varPixelPosition = position - vec2(inGrid.minX, inGrid.minY) + vec2(inGrid.pixelOffsetX, inGrid.pixelOffsetY);
}
`;
const GridNumberVShader = GridShaderShared + `
out vec2 varPixelPosition;
void main()
{
vec2 position = QuadPosition(gl_VertexID, inGrid.minX, inGrid.minY, inGrid.maxX, inGrid.maxY);
vec4 ndc_pos = UVToNDC(inViewport, position);
gl_Position = ndc_pos;
varPixelPosition = position - vec2(inGrid.minX, inGrid.minY) + vec2(inGrid.pixelOffsetX, inGrid.pixelOffsetY);
}
`;
// -------------------------------------------------------------------------------------------------------------------------------
// Fragment Shader
// -------------------------------------------------------------------------------------------------------------------------------
const GridFShader = GridShaderShared + `
// Array of samples
uniform sampler2D inSamples;
uniform float inSamplesLength;
uniform float inFloatsPerSample;
uniform float inNbSamples;
in vec2 varPixelPosition;
out vec4 outColour;
void main()
{
// Font description
float font_width_px = inTextBufferDesc.fontWidth;
float font_height_px = inTextBufferDesc.fontHeight;
float text_buffer_length = inTextBufferDesc.textBufferLength;
// Which row are we on?
float row = Row(varPixelPosition);
vec3 row_colour = RowColour(row);
float text_weight = 0.0;
vec3 text_colour = vec3(0.0);
if (row < inNbSamples)
{
// Unpack colour and depth
int colour_depth = floatBitsToInt(TextureBufferLookup(inSamples, row * inFloatsPerSample + 2.0, inSamplesLength).r);
text_colour.r = float(colour_depth & 255) / 255.0;
text_colour.g = float((colour_depth >> 8) & 255) / 255.0;
text_colour.b = float((colour_depth >> 16) & 255) / 255.0;
float depth = float(colour_depth >> 24);
float text_buffer_offset = TextureBufferLookup(inSamples, row * inFloatsPerSample + 0.0, inSamplesLength).r;
float text_length_chars = TextureBufferLookup(inSamples, row * inFloatsPerSample + 1.0, inSamplesLength).r;
float text_length_px = text_length_chars * font_width_px;
// Pixel position within the row
vec2 pos_in_box_px;
pos_in_box_px.x = varPixelPosition.x;
pos_in_box_px.y = varPixelPosition.y - row * RowHeight;
// Get text at this position
vec2 text_start_px = vec2(4.0 + depth * 10.0, 3.0);
text_weight = LookupText(pos_in_box_px - text_start_px, text_buffer_offset, text_length_chars);
}
outColour = vec4(mix(row_colour, text_colour, text_weight), 1.0);
}
`;
const GridNumberFShader = GridShaderShared + `
// Array of samples
uniform sampler2D inSamples;
uniform float inSamplesLength;
uniform float inFloatsPerSample;
uniform float inNbSamples;
// Offset within the sample
uniform float inNumberOffset;
uniform float inNbFloatChars;
in vec2 varPixelPosition;
out vec4 outColour;
void main()
{
// Font description
float font_width_px = inTextBufferDesc.fontWidth;
float font_height_px = inTextBufferDesc.fontHeight;
float text_buffer_length = inTextBufferDesc.textBufferLength;
// Which row are we on?
float row = Row(varPixelPosition);
vec3 row_colour = RowColour(row);
float text_weight = 0.0;
if (row < inNbSamples)
{
// Pixel position within the row
vec2 pos_in_box_px;
pos_in_box_px.x = varPixelPosition.x;
pos_in_box_px.y = varPixelPosition.y - row * RowHeight;
// Get the number at this pixel
const vec2 text_start_px = vec2(4.0, 3.0);
float number = TextureBufferLookup(inSamples, row * inFloatsPerSample + inNumberOffset, inSamplesLength).r;
text_weight = LookupNumber(pos_in_box_px - text_start_px, number, inNbFloatChars);
}
outColour = vec4(mix(row_colour, vec3(1.0), text_weight), 1.0);
}
`;

View file

@ -1,154 +0,0 @@
const ShaderShared = `#version 300 es
precision mediump float;
struct Viewport
{
float width;
float height;
};
vec2 QuadPosition(int vertex_id, float min_x, float min_y, float max_x, float max_y)
{
// Quad indices are:
//
// 2 3
// +----+
// | |
// +----+
// 0 1
//
vec2 position;
position.x = (vertex_id & 1) == 0 ? min_x : max_x;
position.y = (vertex_id & 2) == 0 ? min_y : max_y;
return position;
}
vec4 UVToNDC(Viewport viewport, vec2 uv)
{
//
// NDC is:
// -1 to 1, left to right
// -1 to 1, bottom to top
//
vec4 ndc_pos;
ndc_pos.x = (uv.x / viewport.width) * 2.0 - 1.0;
ndc_pos.y = 1.0 - (uv.y / viewport.height) * 2.0;
ndc_pos.z = 0.0;
ndc_pos.w = 1.0;
return ndc_pos;
}
vec4 TextureBufferLookup(sampler2D sampler, float index, float length)
{
vec2 uv = vec2((index + 0.5) / length, 0.5);
return texture(sampler, uv);
}
struct TextBufferDesc
{
float fontWidth;
float fontHeight;
float textBufferLength;
};
uniform sampler2D inFontAtlasTexture;
uniform sampler2D inTextBuffer;
uniform TextBufferDesc inTextBufferDesc;
float LookupCharacter(float char_ascii, float pos_x, float pos_y, float font_width_px, float font_height_px)
{
// 2D index of the ASCII character in the font atlas
float char_index_y = floor(char_ascii / 16.0);
float char_index_x = char_ascii - char_index_y * 16.0;
// Start UV of the character in the font atlas
float char_base_uv_x = char_index_x / 16.0;
float char_base_uv_y = char_index_y / 16.0;
// UV within the character itself, scaled to the font atlas
float char_uv_x = pos_x / (font_width_px * 16.0);
float char_uv_y = pos_y / (font_height_px * 16.0);
vec2 uv;
uv.x = char_base_uv_x + char_uv_x;
uv.y = char_base_uv_y + char_uv_y;
// Strip colour and return alpha only
return texture(inFontAtlasTexture, uv).a;
}
float LookupText(vec2 render_pos_px, float text_buffer_offset, float text_length_chars)
{
// Font description
float font_width_px = inTextBufferDesc.fontWidth;
float font_height_px = inTextBufferDesc.fontHeight;
float text_buffer_length = inTextBufferDesc.textBufferLength;
float text_length_px = text_length_chars * font_width_px;
// Text pixel position clamped to the bounds of the full word, allowing leakage to neighbouring NULL characters to pad zeroes
vec2 text_pixel_pos;
text_pixel_pos.x = max(min(render_pos_px.x, text_length_px), -1.0);
text_pixel_pos.y = max(min(render_pos_px.y, font_height_px - 1.0), 0.0);
// Index of the current character in the text buffer
float text_index = text_buffer_offset + floor(text_pixel_pos.x / font_width_px);
// Sample the 1D text buffer to get the ASCII character index
float char_ascii = TextureBufferLookup(inTextBuffer, text_index, text_buffer_length).a * 255.0;
return LookupCharacter(char_ascii,
text_pixel_pos.x - (text_index - text_buffer_offset) * font_width_px,
text_pixel_pos.y,
font_width_px, font_height_px);
}
float NbIntegerCharsForNumber(float number)
{
float number_int = floor(number);
return number_int == 0.0 ? 1.0 : floor(log(number_int) / 2.302585092994046) + 1.0;
}
// Base-10 lookup table for shifting digits of a float to the range 0-9 where they can be rendered
const float g_Multipliers[14] = float[14](
// Decimal part multipliers
1000.0, 100.0, 10.0,
// Zero entry for maintaining the ASCII "." base when rendering the period
0.0,
// Integer part multipliers
1.0, 0.1, 0.01, 0.001, 0.0001, 0.00001, 0.000001, 0.0000001, 0.00000001, 0.000000001 );
float LookupNumber(vec2 render_pos_px, float number, float nb_float_chars)
{
// Font description
float font_width_px = inTextBufferDesc.fontWidth;
float font_height_px = inTextBufferDesc.fontHeight;
float text_buffer_length = inTextBufferDesc.textBufferLength;
float number_integer_chars = NbIntegerCharsForNumber(number);
// Clip
render_pos_px.y = max(min(render_pos_px.y, font_height_px - 1.0), 0.0);
float number_index = floor(render_pos_px.x / font_width_px);
if (number_index >= 0.0 && number_index < number_integer_chars + nb_float_chars)
{
// When we are indexing the period separating integer and decimal, set the base to ASCII "."
// The lookup table stores zero for this entry, multipying with the addend to produce no shift from this base
float base = (number_index == number_integer_chars) ? 46.0 : 48.0;
// Calculate digit using the current number index base-10 shift
float multiplier = g_Multipliers[int(number_integer_chars - number_index) + 3];
float number_shifted_int = floor(number * multiplier);
float number_digit = floor(mod(number_shifted_int, 10.0));
return LookupCharacter(base + number_digit,
render_pos_px.x - number_index * font_width_px,
render_pos_px.y,
font_width_px, font_height_px);
}
return 0.0;
}
`;

View file

@ -1,337 +0,0 @@
const TimelineShaderShared = ShaderShared + `
#define SAMPLE_HEIGHT 16.0
#define SAMPLE_BORDER 2.0
#define SAMPLE_Y_SPACING (SAMPLE_HEIGHT + SAMPLE_BORDER * 2.0)
#define PIXEL_ROUNDED_OFFSETS
struct Container
{
float x0;
float y0;
float x1;
float y1;
};
struct TimeRange
{
float msStart;
float msPerPixel;
};
struct Row
{
float yOffset;
};
uniform Viewport inViewport;
uniform TimeRange inTimeRange;
uniform Container inContainer;
uniform Row inRow;
float PixelOffset(float time_ms)
{
float offset = (time_ms - inTimeRange.msStart) * inTimeRange.msPerPixel;
#ifdef PIXEL_ROUNDED_OFFSETS
return floor(offset);
#else
return offset;
#endif
}
float PixelSize(float time_ms)
{
float size = time_ms * inTimeRange.msPerPixel;
#ifdef PIXEL_ROUNDED_OFFSETS
return floor(size);
#else
return size;
#endif
}
vec4 SampleQuad(int vertex_id, vec4 in_sample_textoffset, float padding, out vec4 out_quad_pos_size_px)
{
// Unpack input data
float ms_start = in_sample_textoffset.x;
float ms_length = in_sample_textoffset.y;
float depth = in_sample_textoffset.z;
// Determine pixel range of the sample
float x0 = PixelOffset(ms_start);
float x1 = x0 + PixelSize(ms_length);
// Calculate box to render
// Ensure no sample is less than one pixel in length and so is always visible
float offset_x = inContainer.x0 + x0 - padding;
float offset_y = inRow.yOffset + (depth - 1.0) * SAMPLE_Y_SPACING + SAMPLE_BORDER - padding;
float size_x = max(x1 - x0, 1.0) + padding * 2.0;
float size_y = SAMPLE_HEIGHT + padding * 2.0;
// Box range clipped to container bounds
float min_x = min(max(offset_x, inContainer.x0), inContainer.x1);
float min_y = min(max(offset_y, inContainer.y0), inContainer.y1);
float max_x = min(max(offset_x + size_x, inContainer.x0), inContainer.x1);
float max_y = min(max(offset_y + size_y, inContainer.y0), inContainer.y1);
// Box quad position in NDC
vec2 position = QuadPosition(vertex_id, min_x, min_y, max_x, max_y);
vec4 ndc_pos = UVToNDC(inViewport, position);
out_quad_pos_size_px.xy = vec2(position.x - offset_x, position.y - offset_y);
out_quad_pos_size_px.zw = vec2(max_x - min_x, max_y - min_y);
return ndc_pos;
}
`;
// -------------------------------------------------------------------------------------------------------------------------------
// Sample Rendering
// -------------------------------------------------------------------------------------------------------------------------------
const TimelineVShader = TimelineShaderShared + `
in vec4 inSample_TextOffset;
in vec4 inColour_TextLength;
out vec4 varColour_TimeMs;
out vec4 varPosInBoxPx_TextEntry;
out float varTimeChars;
void main()
{
// Unpack input data
float ms_length = inSample_TextOffset.y;
float text_buffer_offset = inSample_TextOffset.w;
vec3 box_colour = inColour_TextLength.rgb;
float text_length_chars = inColour_TextLength.w;
// Calculate number of characters required to display the millisecond time
float time_chars = NbIntegerCharsForNumber(ms_length);
// Calculate sample quad vertex positions
vec4 quad_pos_size_px;
gl_Position = SampleQuad(gl_VertexID, inSample_TextOffset, 0.0, quad_pos_size_px);
// Pack for fragment shader
varColour_TimeMs = vec4(box_colour / 255.0, ms_length);
varPosInBoxPx_TextEntry = vec4(quad_pos_size_px.x, quad_pos_size_px.y, text_buffer_offset, text_length_chars);
varTimeChars = time_chars;
}
`;
const TimelineFShader = TimelineShaderShared + `
in vec4 varColour_TimeMs;
in vec4 varPosInBoxPx_TextEntry;
in float varTimeChars;
out vec4 outColour;
void main()
{
// Font description
float font_width_px = inTextBufferDesc.fontWidth;
float font_height_px = inTextBufferDesc.fontHeight;
// Text range in the text buffer
vec2 pos_in_box_px = varPosInBoxPx_TextEntry.xy;
float text_buffer_offset = varPosInBoxPx_TextEntry.z;
float text_length_chars = varPosInBoxPx_TextEntry.w;
float text_length_px = text_length_chars * font_width_px;
// Text placement offset within the box
const vec2 text_start_px = vec2(10.0, 3.0);
vec3 box_colour = varColour_TimeMs.rgb;
// Add a subtle border to the box so that you can visually separate samples when they are next to each other
vec2 top_left = min(pos_in_box_px.xy, 2.0);
float both = min(top_left.x, top_left.y);
box_colour *= (0.8 + both * 0.1);
float text_weight = 0.0;
// Are we over the time number or the text?
float text_end_px = text_start_px.x + text_length_px;
float number_start_px = text_end_px + font_width_px * 2.0;
if (pos_in_box_px.x > number_start_px)
{
vec2 time_pixel_pos;
time_pixel_pos.x = pos_in_box_px.x - number_start_px;
time_pixel_pos.y = max(min(pos_in_box_px.y - text_start_px.y, font_height_px - 1.0), 0.0);
// Time number
float time_ms = varColour_TimeMs.w;
float time_index = floor(time_pixel_pos.x / font_width_px);
if (time_index < varTimeChars + 4.0)
{
text_weight = LookupNumber(time_pixel_pos, time_ms, 4.0);
}
// " ms" label at the end of the time
else if (time_index < varTimeChars + 7.0)
{
const float ms[3] = float[3] ( 32.0, 109.0, 115.0 );
float char = ms[int(time_index - (varTimeChars + 4.0))];
text_weight = LookupCharacter(char, time_pixel_pos.x - time_index * font_width_px, time_pixel_pos.y, font_width_px, font_height_px);
}
}
else
{
text_weight = LookupText(pos_in_box_px - text_start_px, text_buffer_offset, text_length_chars);
}
// Blend text onto the box
vec3 text_colour = vec3(0.0, 0.0, 0.0);
outColour = vec4(mix(box_colour, text_colour, text_weight), 1.0);
}
`;
// -------------------------------------------------------------------------------------------------------------------------------
// Sample Highlights
// -------------------------------------------------------------------------------------------------------------------------------
const TimelineHighlightVShader = TimelineShaderShared + `
uniform float inStartMs;
uniform float inLengthMs;
uniform float inDepth;
out vec4 varPosSize;
void main()
{
// Calculate sample quad vertex positions
gl_Position = SampleQuad(gl_VertexID, vec4(inStartMs, inLengthMs, inDepth, 0.0), 1.0, varPosSize);
}
`;
const TimelineHighlightFShader = TimelineShaderShared + `
// TODO(don): Vector uniforms, please!
uniform float inColourR;
uniform float inColourG;
uniform float inColourB;
in vec4 varPosSize;
out vec4 outColour;
void main()
{
// Rounded pixel co-ordinates interpolating across the sample
vec2 pos = floor(varPosSize.xy);
// Sample size in pixel co-ordinates
vec2 size = floor(varPosSize.zw);
// Highlight thickness
float t = 2.0;
// Distance along axes to highlight edges
vec2 dmin = abs(pos - 0.0);
vec2 dmax = abs(pos - (size - 1.0));
// Take the closest distance
float dx = min(dmin.x, dmax.x);
float dy = min(dmin.y, dmax.y);
float d = min(dx, dy);
// Invert the distance and clamp to thickness
d = (t + 1.0) - min(d, t + 1.0);
// Scale with thickness for uniform intensity
d = d / (t + 1.0);
outColour = vec4(inColourR * d, inColourG * d, inColourB * d, d);
}
`;
// -------------------------------------------------------------------------------------------------------------------------------
// GPU->CPU Sample Sources
// -------------------------------------------------------------------------------------------------------------------------------
const TimelineGpuToCpuVShader = TimelineShaderShared + `
uniform float inStartMs;
uniform float inLengthMs;
uniform float inDepth;
out vec4 varPosSize;
void main()
{
// Calculate sample quad vertex positions
gl_Position = SampleQuad(gl_VertexID, vec4(inStartMs, inLengthMs, inDepth, 0.0), 1.0, varPosSize);
}
`;
const TimelineGpuToCpuFShader = TimelineShaderShared + `
in vec4 varPosSize;
out vec4 outColour;
void main()
{
// Rounded pixel co-ordinates interpolating across the sample
vec2 pos = floor(varPosSize.xy);
// Sample size in pixel co-ordinates
vec2 size = floor(varPosSize.zw);
// Distance to centre line, bumped out every period to create a dash
float dc = abs(pos.y - size.y / 2.0);
dc += (int(pos.x / 3.0) & 1) == 0 ? 100.0 : 0.0;
// Min with the start line
float ds = abs(pos.x - 0.0);
float d = min(dc, ds);
// Invert the distance for highlight
d = 1.0 - min(d, 1.0);
outColour = vec4(d, d, d, d);
}
`;
// -------------------------------------------------------------------------------------------------------------------------------
// Background
// -------------------------------------------------------------------------------------------------------------------------------
const TimelineBackgroundVShader = TimelineShaderShared + `
uniform float inYOffset;
out vec2 varPosition;
void main()
{
// Container quad position in NDC
vec2 position = QuadPosition(gl_VertexID, inContainer.x0, inContainer.y0, inContainer.x1, inContainer.y1);
gl_Position = UVToNDC(inViewport, position);
// Offset Y with scroll position
varPosition = vec2(position.x, position.y - inYOffset);
}
`;
const TimelineBackgroundFShader = TimelineShaderShared + `
in vec2 varPosition;
out vec4 outColour;
void main()
{
vec2 pos = floor(varPosition);
float f = round(fract(pos.y / SAMPLE_Y_SPACING) * SAMPLE_Y_SPACING);
float g = f >= 1.0 && f <= (SAMPLE_Y_SPACING - 2.0) ? 0.30 : 0.23;
outColour = vec4(g, g, g, 1.0);
}
`;

View file

@ -1,33 +0,0 @@
// -------------------------------------------------------------------------------------------------------------------------------
// Vertex Shader
// -------------------------------------------------------------------------------------------------------------------------------
const WindowVShader = ShaderShared + `
uniform Viewport inViewport;
uniform float minX;
uniform float minY;
uniform float maxX;
uniform float maxY;
void main()
{
vec2 position = QuadPosition(gl_VertexID, minX, minY, maxX, maxY);
gl_Position = UVToNDC(inViewport, position);
}
`;
// -------------------------------------------------------------------------------------------------------------------------------
// Fragment Shader
// -------------------------------------------------------------------------------------------------------------------------------
const WindowFShader = ShaderShared + `
out vec4 outColour;
void main()
{
outColour = vec4(1.0, 1.0, 1.0, 0.0);
}
`;

Some files were not shown because too many files have changed in this diff Show more