derive OHP3D from ShaderShark v_0
authorFrantišek Kučera <franta-hg@frantovo.cz>
Wed, 27 Dec 2023 01:50:38 +0100
branchv_0
changeset 29 dc3c102e1264
parent 28 4cbd9c0beb4c
child 30 02972f051744
derive OHP3D from ShaderShark
AllocatedBuffer.h
Buffer.h
CLIParser.h
Configuration.h
EPoll.h
FileMonitor.cpp
FileMonitor.h
ImageLoader.cpp
ImageLoader.h
Logger.h
Makefile
MappedFile.h
OHP3D.cpp
OHP3D.h
Program.cpp
Program.h
Shader.cpp
Shader.h
Shark.cpp
Shark.h
Texture.cpp
Texture.h
XAttrs.cpp
XAttrs.h
bash-completion.sh
nbproject/configurations.xml
nbproject/project.xml
ohp3d.cpp
opengl.h
shader-shark.cpp
textures/default.png
x11.h
--- a/AllocatedBuffer.h	Tue Dec 26 23:46:45 2023 +0100
+++ b/AllocatedBuffer.h	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/Buffer.h	Tue Dec 26 23:46:45 2023 +0100
+++ b/Buffer.h	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/CLIParser.h	Tue Dec 26 23:46:45 2023 +0100
+++ b/CLIParser.h	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/Configuration.h	Tue Dec 26 23:46:45 2023 +0100
+++ b/Configuration.h	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/EPoll.h	Tue Dec 26 23:46:45 2023 +0100
+++ b/EPoll.h	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/FileMonitor.cpp	Tue Dec 26 23:46:45 2023 +0100
+++ b/FileMonitor.cpp	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/FileMonitor.h	Tue Dec 26 23:46:45 2023 +0100
+++ b/FileMonitor.h	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/ImageLoader.cpp	Tue Dec 26 23:46:45 2023 +0100
+++ b/ImageLoader.cpp	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/ImageLoader.h	Tue Dec 26 23:46:45 2023 +0100
+++ b/ImageLoader.h	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/Logger.h	Tue Dec 26 23:46:45 2023 +0100
+++ b/Logger.h	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/Makefile	Tue Dec 26 23:46:45 2023 +0100
+++ b/Makefile	Wed Dec 27 01:50:38 2023 +0100
@@ -1,4 +1,4 @@
-# ShaderShark
+# OHP3D
 # Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
 #
 # This program is free software: you can redistribute it and/or modify
@@ -13,19 +13,19 @@
 # You should have received a copy of the GNU General Public License
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
-all: build/shader-shark
+all: build/ohp3d
 
 .PHONY: all clean run
 
 clean:
 	rm -rf build
 
-run: build/shader-shark
-	SHADER_SHARK_DATA_DIR=. $(<)
+run: build/ohp3d
+	OHP3D_DATA_DIR=. $(<)
 
 SRC= \
-    Shark.cpp \
-    shader-shark.cpp \
+    OHP3D.cpp \
+    ohp3d.cpp \
     ImageLoader.cpp \
     Texture.cpp \
     Shader.cpp \
@@ -33,7 +33,7 @@
     XAttrs.cpp \
     FileMonitor.cpp
 
-build/shader-shark: $(SRC) *.h
+build/ohp3d: $(SRC) *.h
 	mkdir -p build
 	$(CXX) -std=c++20 -g -o $(@) $(SRC) $$(pkg-config --cflags --libs \
 	    epoxy x11 glu glm Magick++)
--- a/MappedFile.h	Tue Dec 26 23:46:45 2023 +0100
+++ b/MappedFile.h	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OHP3D.cpp	Wed Dec 27 01:50:38 2023 +0100
@@ -0,0 +1,694 @@
+/**
+ * OHP3D
+ * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <iostream>
+#include <iomanip>
+#include <string>
+#include <charconv>
+#include <memory>
+#include <functional>
+#include <sstream>
+#include <vector>
+
+#include "x11.h"
+#include "opengl.h"
+#include "EPoll.h"
+#include "Logger.h"
+#include "MappedFile.h"
+#include "ImageLoader.h"
+#include "Texture.h"
+#include "Shader.h"
+#include "Program.h"
+#include "FileMonitor.h"
+#include "XAttrs.h"
+
+#include "OHP3D.h"
+
+class OHP3D::Impl {
+public:
+
+	struct {
+		GLint aVertexXYZ = -2;
+		GLint aTextureXY = -2;
+
+		GLint fColor = -2;
+
+		GLint uModel = -2;
+		GLint uView = -2;
+		GLint uProjection = -2;
+		GLint uTexture = -2;
+		GLint uTextureScale = -2;
+	} ProgAttr;
+
+	struct {
+		float yaw = -90.f;
+		float pitch = 0.f;
+		float roll = 0.f;
+		float fov = 45.0f;
+		glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
+		glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
+		glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
+
+		void adjustFov(float diff) {
+			fov += diff;
+			if (fov < 1.0f) fov = 1.0f;
+			else if (fov > 120.0f) fov = 120.0f;
+			std::cerr << "field of view: " << fov << " °" << std::endl;
+		}
+
+		void moveForward(const float cameraSpeed) {
+			cameraPos += cameraSpeed * cameraFront;
+		}
+
+		void moveBackward(const float cameraSpeed) {
+			cameraPos -= cameraSpeed * cameraFront;
+		}
+
+		void moveLeft(const float cameraSpeed) {
+			cameraPos -= glm::normalize(
+					glm::cross(cameraFront, cameraUp)) * cameraSpeed;
+		}
+
+		void moveRight(const float cameraSpeed) {
+			cameraPos += glm::normalize(
+					glm::cross(cameraFront, cameraUp)) * cameraSpeed;
+		}
+
+		void moveUp(const float cameraSpeed) {
+			cameraPos += cameraSpeed * glm::normalize(cameraUp);
+		}
+
+		void moveDown(const float cameraSpeed) {
+			cameraPos -= cameraSpeed * glm::normalize(cameraUp);
+		}
+
+		void updateCameraFrontAndUp() {
+			std::cerr << "--- updateCameraFrontAndUp() --------" << std::endl;
+			dump("pitch, yaw, roll", glm::vec3(pitch, yaw, roll));
+			dump("cameraPos", cameraPos);
+			dump("cameraFront", cameraFront);
+			const auto pitchR = glm::radians(pitch); // around X axis
+			const auto yawR = glm::radians(yaw); //     around Y axis
+			const auto rollR = glm::radians(roll); //   around Z axis
+
+			cameraFront.x = cos(pitchR) * cos(yawR);
+			cameraFront.y = sin(pitchR);
+			cameraFront.z = cos(pitchR) * sin(yawR);
+			cameraFront = glm::normalize(cameraFront);
+			dump("cameraFront", cameraFront);
+			dump("cameraUp", cameraUp);
+
+			// TODO: review ROLL rotation and default angle
+			glm::mat4 rollMatrix = glm::rotate(
+					glm::mat4(1.0f), rollR, cameraFront);
+			cameraUp = glm::mat3(rollMatrix) * glm::vec3(0., 1., 0.);
+			dump("cameraUp", cameraUp);
+			std::cerr << "-------------------------------------" << std::endl;
+		}
+
+		void limitPitch() {
+			if (pitch > +89.0f) pitch = +89.0f;
+			if (pitch < -89.0f) pitch = -89.0f;
+		}
+
+		void turnLeft(const float angleSpeed) {
+			yaw -= angleSpeed;
+			updateCameraFrontAndUp();
+		}
+
+		void turnRight(const float angleSpeed) {
+			yaw += angleSpeed;
+			updateCameraFrontAndUp();
+		}
+
+		void turnUp(const float angleSpeed) {
+			pitch += angleSpeed;
+			limitPitch();
+			updateCameraFrontAndUp();
+		}
+
+		void turnDown(const float angleSpeed) {
+			pitch -= angleSpeed;
+			limitPitch();
+			updateCameraFrontAndUp();
+		}
+
+		void rollLeft(const float angleSpeed) {
+			roll += angleSpeed;
+			updateCameraFrontAndUp();
+		}
+
+		void rollRight(const float angleSpeed) {
+			roll -= angleSpeed;
+			updateCameraFrontAndUp();
+		}
+
+	} initialCtx, ctx;
+
+	Display* dpy;
+	Window win;
+	XVisualInfo* vi;
+	GLXContext glc;
+
+	FileMonitor fileMonitor;
+	std::vector<WatchedFile> watchedFiles;
+	ImageLoader imageLoader;
+	std::vector<std::shared_ptr<Shader>> shaders;
+	std::shared_ptr<Program> shaderProgram;
+	std::vector<std::shared_ptr<Texture>> textures;
+
+	Configuration cfg;
+	std::ostream& logOutput = std::cerr;
+
+	Impl(Configuration cfg) : cfg(cfg) {
+	}
+
+	void run();
+	void clear();
+	void runShaders();
+	Window getRootWindow(Window defaultValue);
+	void log(LogLevel level, std::string message);
+	int setNonBlocking(int fd);
+	void loadVertices();
+	void parametrizeTexture(std::shared_ptr<Texture> tex);
+	bool reloadTexture(const std::string& fileName);
+	void loadTextures();
+	void loadShaders();
+	void updateVariableLocations();
+	bool reloadShader(const std::string& fileName);
+	void setTitle(const std::string& suffix = "");
+	static const std::string getDefaultFile(const std::string& relativePath);
+
+};
+
+OHP3D::OHP3D(const Configuration& configuration) :
+impl(new Impl(configuration)) {
+}
+
+OHP3D::~OHP3D() {
+	impl->textures.clear();
+	impl->shaders.clear();
+	impl->shaderProgram = nullptr;
+	XFree(impl->vi);
+	glXMakeCurrent(impl->dpy, None, NULL);
+	glXDestroyContext(impl->dpy, impl->glc);
+	XDestroyWindow(impl->dpy, impl->win);
+	XCloseDisplay(impl->dpy);
+	delete impl;
+	// std::cerr << "~OHP3D()" << std::endl;
+}
+
+void OHP3D::Impl::setTitle(const std::string& suffix) {
+	std::stringstream title;
+	title << "OHP3D";
+	if (suffix.size()) title << ": " << suffix.c_str();
+	XStoreName(dpy, win, title.str().c_str());
+	XFlush(dpy);
+}
+
+void OHP3D::run() {
+	impl->run();
+}
+
+void OHP3D::Impl::run() {
+	dpy = XOpenDisplay(NULL);
+
+	if (dpy == NULL) throw std::logic_error("Unable to connect to X server");
+
+	GLint att[] = {GLX_RGBA, GLX_DEPTH_SIZE, 24, GLX_DOUBLEBUFFER, None};
+	vi = glXChooseVisual(dpy, 0, att);
+	Window root = DefaultRootWindow(dpy);
+	Window parent = cfg.rootWindow ? cfg.rootWindow : root;
+
+	XSetWindowAttributes swa;
+	swa.colormap = XCreateColormap(dpy, parent, vi->visual, AllocNone);
+	swa.event_mask = ExposureMask | KeyPressMask | PointerMotionMask
+			| ButtonPressMask
+			| StructureNotifyMask;
+
+	bool full = false;
+	unsigned int width = 1600;
+	unsigned int height = 1200;
+	if (parent != root) {
+		XWindowAttributes parentAttr;
+		XGetWindowAttributes(dpy, parent, &parentAttr);
+		width = parentAttr.width;
+		height = parentAttr.height;
+	}
+
+	win = XCreateWindow(
+			dpy, parent, 0, 0, width, height, 0,
+			vi->depth, InputOutput, vi->visual,
+			CWColormap | CWEventMask, &swa);
+
+	XMapWindow(dpy, win);
+	setTitle();
+	setX11PID(dpy, win);
+	// XSetWindowBackground(dpy, win, 0) vs. glClearColor()
+
+	glc = glXCreateContext(dpy, vi, NULL, GL_TRUE);
+	glXMakeCurrent(dpy, win, glc);
+
+	glEnable(GL_DEPTH_TEST);
+	glEnable(GL_BLEND);
+	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+	clear();
+	glXSwapBuffers(dpy, win);
+
+
+	// Load GLSL shaders:
+	loadShaders();
+	loadTextures();
+	loadVertices();
+
+	auto toggleFullscreen = [&]() {
+		full = setFullscreen(dpy, win, !full);
+	};
+
+	auto resetView = [&]() {
+		ctx = initialCtx;
+		ctx.updateCameraFrontAndUp();
+	};
+
+	// root can reize our window
+	// or we can listen to root resize and then resize our window ourselves
+	bool listenToRootResizes = true;
+	if (listenToRootResizes) XSelectInput(dpy, parent, StructureNotifyMask);
+
+	bool keepRunningX11 = true;
+	int x11fd = XConnectionNumber(dpy);
+	EPoll epoll;
+	epoll.add(x11fd);
+	epoll.add(fileMonitor.getFD());
+	try {
+		epoll.add(setNonBlocking(STDIN_FILENO));
+	} catch (const EPoll::Exception& e) {
+		logOutput << "Will not monitor events on STDIN: " << e.what() << "\n";
+	}
+
+	// rended the 3D scene even before the first event:
+	runShaders();
+	glXSwapBuffers(dpy, win);
+
+	for (XEvent xev; keepRunningX11;) {
+		int epollEventCount = epoll.wait();
+		//std::cout << "trace: epoll.wait() = " << epollEventCount << std::endl;
+		for (int epollEvent = 0; epollEvent < epollEventCount; epollEvent++) {
+			bool redraw = false;
+			if (epoll[epollEvent].data.fd == x11fd) {
+				if (!XPending(dpy)) {
+					// otherwise STDIN events are held until the first X11 event
+					logOutput << "trace: no pending X11 event" << std::endl;
+					break;
+				}
+process_x11_event:
+				XWindowAttributes gwa;
+				XNextEvent(dpy, &xev);
+
+				if (xev.type == Expose) {
+					std::cout << "XEvent: Expose" << std::endl;
+					XGetWindowAttributes(dpy, win, &gwa);
+					glViewport(0, 0, gwa.width, gwa.height);
+					redraw = true;
+				} else if (xev.type == KeyPress) {
+					DecodedKey key = decodeKeycode(dpy, xev.xkey.keycode);
+					std::cout << "XEvent: KeyPress:"
+							<< " keycode=" << key.code
+							<< " key=" << key.name
+							<< std::endl;
+
+					const float cSp = 0.05f; // camera speed
+					const float aSp = 5.f; // angle speed
+
+					if (key.matches(XK_q, XK_Escape)) keepRunningX11 = false;
+					else if (key.matches(XK_Left, XK_s)) ctx.turnLeft(aSp);
+					else if (key.matches(XK_Right, XK_f)) ctx.turnRight(aSp);
+					else if (key.matches(XK_Up, XK_e)) ctx.moveForward(cSp);
+					else if (key.matches(XK_Down, XK_d)) ctx.moveBackward(cSp);
+					else if (key.matches(XK_w)) ctx.rollLeft(aSp);
+					else if (key.matches(XK_r)) ctx.rollRight(aSp);
+					else if (key.matches(XK_t)) ctx.turnUp(aSp);
+					else if (key.matches(XK_g)) ctx.turnDown(aSp);
+					else if (key.matches(XK_m)) ctx.moveLeft(cSp);
+					else if (key.matches(XK_comma)) ctx.moveRight(cSp);
+					else if (key.matches(XK_l)) ctx.moveUp(cSp);
+					else if (key.matches(XK_period)) ctx.moveDown(cSp);
+					else if (key.matches(XK_j)) ctx.moveLeft(cSp * 5);
+					else if (key.matches(XK_k)) ctx.moveRight(cSp * 5);
+					else if (key.matches(XK_u)) ctx.moveLeft(cSp * 10);
+					else if (key.matches(XK_i)) ctx.moveRight(cSp * 10);
+					else if (key.matches(XK_x)) resetView();
+					else if (key.matches(XK_F11, XK_y)) toggleFullscreen();
+					redraw = true;
+				} else if (xev.type == ButtonPress) {
+					std::cout << "XEvent: ButtonPress:"
+							<< " button=" << xev.xbutton.button
+							<< std::endl;
+					if (xev.xbutton.button == 1);
+					else if (xev.xbutton.button == 4) ctx.adjustFov(-1.0);
+					else if (xev.xbutton.button == 5) ctx.adjustFov(+1.0);
+					else if (xev.xbutton.button == 8) resetView();
+					else if (xev.xbutton.button == 9) keepRunningX11 = false;
+					redraw = true;
+				} else if (xev.type == MotionNotify) {
+					// printCursorInfo(xev.xmotion);
+				} else if (xev.type == ConfigureNotify) {
+					std::cout << "XEvent: ConfigureNotify:"
+							<< " window=" << xev.xconfigure.window
+							<< " height=" << xev.xconfigure.height
+							<< " width=" << xev.xconfigure.width
+							<< std::endl;
+					if (listenToRootResizes
+							&& xev.xconfigure.window == parent) {
+						XResizeWindow(dpy, win,
+								xev.xconfigure.width, xev.xconfigure.height);
+					}
+				} else if (xev.type == UnmapNotify) {
+					std::cout << "XEvent: UnmapNotify" << std::endl;
+				} else if (xev.type == DestroyNotify) {
+					std::cout << "XEvent: DestroyNotify → finish" << std::endl;
+					break;
+				} else {
+					std::cout << "XEvent: type=" << xev.type << std::endl;
+				}
+				if (XPending(dpy)) goto process_x11_event;
+			} else if (epoll[epollEvent].data.fd == STDIN_FILENO) {
+				int epollFD = epoll[epollEvent].data.fd;
+				logOutput << "other event: fd=" << epollFD << " data=";
+				for (char ch; read(epollFD, &ch, 1) > 0;) {
+					std::stringstream msg;
+					msg
+							<< std::hex
+							<< std::setfill('0')
+							<< std::setw(2)
+							<< (int) ch;
+					logOutput << msg.str();
+				}
+				logOutput << std::endl;
+
+			} else if (epoll[epollEvent].data.fd == fileMonitor.getFD()) {
+				std::cout << "FileMonitor event:" << std::endl;
+				for (FileEvent fe; fileMonitor.readEvent(fe);) {
+					logOutput << "   "
+							<< " file=" << fe.fileName
+							<< " mask=" << fe.mask
+							<< std::endl;
+					try {
+						redraw |= reloadTexture(fe.fileName);
+						redraw |= reloadShader(fe.fileName);
+						setTitle();
+					} catch (const std::exception& e) {
+						setTitle("[ERROR]");
+						logOutput << "error while reloading '"
+								<< fe.fileName.c_str()
+								<< "': " << e.what() << std::endl;
+					}
+				}
+			} else {
+				logOutput
+						<< "error: event on an unexpected FD: "
+						<< epoll[epollEvent].data.fd
+						<< std::endl;
+			}
+
+			if (redraw) {
+				runShaders();
+				glXSwapBuffers(dpy, win);
+			}
+		}
+	}
+}
+
+void OHP3D::Impl::clear() {
+	glClearColor(
+			(cfg.backgroundColor >> 16 & 0xFF) / 256.,
+			(cfg.backgroundColor >> 8 & 0xFF) / 256.,
+			(cfg.backgroundColor & 0xFF) / 256.,
+			1.0);
+	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+}
+
+void OHP3D::Impl::runShaders() {
+	shaderProgram->use();
+	checkError(&std::cerr);
+
+	clear();
+
+	GLint viewport[4];
+	glGetIntegerv(GL_VIEWPORT, viewport);
+	GLfloat width = viewport[2];
+	GLfloat height = viewport[3];
+
+	glm::mat4 projection = glm::perspective(
+			glm::radians(ctx.fov),
+			width / height,
+			0.1f, 100.0f);
+	glUniformMatrix4fv(ProgAttr.uProjection, 1, GL_FALSE, &projection[0][0]);
+
+	glm::mat4 view = glm::lookAt(
+			ctx.cameraPos,
+			ctx.cameraPos + ctx.cameraFront,
+			ctx.cameraUp);
+	glUniformMatrix4fv(ProgAttr.uView, 1, GL_FALSE, &view[0][0]);
+
+	// glBindVertexArray(vao);
+
+	glm::mat4 model = glm::mat4(1.0f); // identity matrix
+	glUniformMatrix4fv(ProgAttr.uModel, 1, GL_FALSE, &model[0][0]);
+
+	// TODO: draw a rectangle for each texture
+	glUniform1f(ProgAttr.uTextureScale, textures[0]->getScale());
+
+	glDrawArrays(GL_TRIANGLES, 0, 2 * 3); // see loadVertices()
+	std::cerr << "GLSL: glDrawArrays()" << std::endl;
+}
+
+void OHP3D::Impl::log(LogLevel level, std::string message) {
+	::log(logOutput, level, message);
+}
+
+int OHP3D::Impl::setNonBlocking(int fd) {
+	int flags = fcntl(fd, F_GETFL, 0);
+	fcntl(fd, F_SETFL, flags | O_NONBLOCK);
+	return fd;
+}
+
+void OHP3D::Impl::loadVertices() {
+	for (int i = 0; i < textures.size(); i++) {
+		std::shared_ptr<Texture> tex = textures[i];
+		// TODO: draw a rectangle for each texture
+		GLfloat ratio = tex->getRatio();
+		const std::vector<GLfloat> vertices = {
+			// Vertex XYZ                      Texture XY
+			-0.80f * ratio, +0.80f, +0.0, /**/ 0.0, 0.0,
+			+0.80f * ratio, +0.80f, +0.0, /**/ 1.0, 0.0,
+			-0.80f * ratio, -0.80f, +0.0, /**/ 0.0, 1.0,
+
+			-0.80f * ratio, -0.80f, +0.0, /**/ 0.0, 1.0,
+			+0.80f * ratio, -0.80f, +0.0, /**/ 1.0, 1.0,
+			+0.80f * ratio, +0.80f, +0.0, /**/ 1.0, 0.0,
+
+			// see glDrawArrays(), where we set start offset and count
+		};
+
+		// Vertex data:
+		glVertexAttribPointer(ProgAttr.aVertexXYZ, 3, // vertex items
+				GL_FLOAT, GL_FALSE, 5 * sizeof (float),
+				(void*) 0);
+		glEnableVertexAttribArray(ProgAttr.aVertexXYZ);
+
+		// Texture positions:
+		glVertexAttribPointer(ProgAttr.aTextureXY, 2, // texture items
+				GL_FLOAT, GL_FALSE, 5 * sizeof (float),
+				(void*) (3 * sizeof (float)));
+		glEnableVertexAttribArray(ProgAttr.aTextureXY);
+
+		glBufferData(GL_ARRAY_BUFFER,
+				vertices.size() * sizeof (vertices[0]),
+				vertices.data(),
+				GL_STATIC_DRAW);
+		// GL_STATIC_DRAW:
+		//   The vertex data will be uploaded once
+		//   and drawn many times(e.g. the world).
+		// GL_DYNAMIC_DRAW:
+		//   The vertex data will be created once, changed from
+		// 	 time to time, but drawn many times more than that.
+		// GL_STREAM_DRAW:
+		//   The vertex data will be uploaded once and drawn once.
+
+		// see also glBindBuffer(GL_ARRAY_BUFFER, vbo); where we set current VBO
+	}
+}
+
+const std::string
+OHP3D::Impl::getDefaultFile(const std::string& relativePath) {
+	const char* envName = "OHP3D_DATA_DIR";
+	const char* envValue = ::getenv(envName);
+	if (envValue) {
+		return std::string(envValue) + "/" + relativePath;
+	} else {
+		throw std::invalid_argument(std::string("Configure $") + envName
+				+ " in order to use defaults"
+				" or specify textures and shaders as parameters");
+	}
+}
+
+void OHP3D::Impl::parametrizeTexture(std::shared_ptr<Texture> tex) {
+	XAttrs xa(tex->getFileName());
+	std::string magf = xa["ohp3d.texture.mag-filter"];
+	std::string minf = xa["ohp3d.texture.min-filter"];
+	std::string scale = xa["ohp3d.texture.scale"];
+
+	auto GLT2D = GL_TEXTURE_2D;
+	auto MAG = GL_TEXTURE_MAG_FILTER;
+	auto MIN = GL_TEXTURE_MIN_FILTER;
+
+	if (magf == "linear") glTexParameteri(GLT2D, MAG, GL_LINEAR);
+	else if (magf == "nearest") glTexParameteri(GLT2D, MAG, GL_NEAREST);
+
+	if (minf == "linear") glTexParameteri(GLT2D, MIN, GL_LINEAR);
+	else if (minf == "nearest") glTexParameteri(GLT2D, MIN, GL_NEAREST);
+
+	if (scale.size()) {
+		float sc;
+		if (std::from_chars(scale.data(), scale.data() + scale.size(), sc).ec
+				== std::errc{}) tex->setScale(sc);
+		else std::cerr << "Invalid texture scale value - expecting float\n";
+		// tex->setScale(std::stof(scale)); // locale-dependent (. vs ,)
+	}
+}
+
+void OHP3D::Impl::loadTextures() {
+	// Load default texture if there is no configured:
+	if (cfg.textures.empty())
+		cfg.textures.push_back({getDefaultFile("textures/default.png")});
+
+	for (const Configuration::Texture& tex : cfg.textures) {
+		std::shared_ptr<ImageLoader::ImageBuffer>
+				img(imageLoader.loadImage(MappedFile(tex.fileName)));
+		textures.push_back(std::make_shared<Texture>(
+				img->width, img->height, *img, tex.fileName));
+		parametrizeTexture(textures.back());
+		// static const uint32_t watchMask = IN_CLOSE_WRITE | IN_ATTRIB;
+		// watchedFiles.push_back(fileMonitor.watch(tex.fileName, watchMask));
+		watchedFiles.push_back(fileMonitor.watch(tex.fileName));
+		// TODO: review texture loading and binding
+		// works even without this - default texture
+		// glUniform1i(..., ...);
+		// checkError(&std::cerr);
+	}
+}
+
+bool OHP3D::Impl::reloadTexture(const std::string& fileName) {
+	for (std::shared_ptr<Texture> tex : textures) {
+		if (tex->getFileName() == fileName) {
+			std::shared_ptr<ImageLoader::ImageBuffer>
+					img(imageLoader.loadImage(MappedFile(fileName)));
+			tex->update(img->width, img->height, *img);
+			parametrizeTexture(tex);
+			loadVertices();
+			return true;
+		}
+	}
+	return false;
+}
+
+void OHP3D::Impl::loadShaders() {
+	// Vertex Array Object (VAO)
+	GLuint vao;
+	glGenVertexArrays(1, &vao);
+	glBindVertexArray(vao);
+	// VAO - something like context for bound data/variables
+	// We can switch multiple VAOs. VAO can contain multiple VBOs.
+	// what-are-vertex-array-objects
+
+	// Vertex Buffer Object (VBO):
+	GLuint vbo;
+	glGenBuffers(1, &vbo);
+	glBindBuffer(GL_ARRAY_BUFFER, vbo);
+
+	{
+		// Load default shaders if there are no configured:
+		int vc = 0;
+		int fc = 0;
+		auto& ss = cfg.shaders;
+		for (const auto& s : ss) if (s.type == "vertex") vc++;
+		for (const auto& s : ss) if (s.type == "fragment") fc++;
+		auto& d = getDefaultFile;
+		if (vc == 0) ss.push_back({d("shaders/default.vert"), "vertex"});
+		if (fc == 0) ss.push_back({d("shaders/default.frag"), "fragment"});
+	}
+
+	shaderProgram = std::make_shared<Program>();
+
+	// glBindFragDataLocation(program, 0, "outColor");
+	// glBindAttribLocation(program, LOC.input, "vertices");
+
+	for (const Configuration::Shader definition : cfg.shaders) {
+		Shader::Type type;
+		std::string fileName = definition.fileName;
+		if (definition.type == "fragment") type = Shader::Type::FRAGMENT;
+		else if (definition.type == "vertex") type = Shader::Type::VERTEX;
+		else throw std::invalid_argument("unsupported shader type");
+
+		MappedFile file(fileName);
+		std::shared_ptr<Shader> shader = std::make_shared<Shader>(
+				type, file, fileName);
+
+		shaderProgram->attachShader(*shader.get());
+		shaders.push_back(shader);
+		watchedFiles.push_back(fileMonitor.watch(fileName));
+		std::cerr << "GLSL loaded: " << fileName.c_str() << std::endl;
+		// We may detach and delete shaders,
+		// but our shaders are small, so we keep them for later reloading.
+	}
+
+	shaderProgram->link();
+	updateVariableLocations();
+	// listVariables(program);
+	std::cerr << "GLSL shader count: " << shaders.size() << std::endl;
+}
+
+void OHP3D::Impl::updateVariableLocations() {
+	// GLSL compiler does very efficient / aggressive optimization.
+	// Attributes and uniforms that are not used in the shader are deleted.
+	// And even if we e.g. read color from a texture and overwrite it,
+	// the variable is still deleted and considered „inactive“.
+	// Functions glGetAttribLocation() and glGetUniformLocation() return -1.
+	ProgAttr.aVertexXYZ = shaderProgram->getAttribLocation("aVertexXYZ");
+	ProgAttr.aTextureXY = shaderProgram->getAttribLocation("aTextureXY");
+	ProgAttr.uModel = shaderProgram->getUniformLocation("uModel");
+	ProgAttr.uView = shaderProgram->getUniformLocation("uView");
+	ProgAttr.uProjection = shaderProgram->getUniformLocation("uProjection");
+	ProgAttr.uTexture = shaderProgram->getUniformLocation("uTexture");
+	ProgAttr.uTextureScale = shaderProgram->getUniformLocation("uTextureScale");
+	ProgAttr.fColor = shaderProgram->getFragDataLocation("fColor");
+	shaderProgram->bindFragDataLocation("fColor", ProgAttr.fColor);
+}
+
+bool OHP3D::Impl::reloadShader(const std::string& fileName) {
+	for (auto shader : shaders) {
+		if (shader->getFileName() == fileName) {
+			shader->update(MappedFile(fileName));
+			shaderProgram->link();
+			updateVariableLocations();
+			return true;
+		}
+	}
+	return false;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OHP3D.h	Wed Dec 27 01:50:38 2023 +0100
@@ -0,0 +1,32 @@
+/**
+ * OHP3D
+ * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "Configuration.h"
+
+class OHP3D {
+public:
+	OHP3D(const Configuration& cfg);
+	virtual ~OHP3D();
+	void run();
+private:
+	class Impl;
+	Impl* impl;
+	OHP3D(const OHP3D&) = delete;
+	OHP3D& operator=(const OHP3D&) = delete;
+};
--- a/Program.cpp	Tue Dec 26 23:46:45 2023 +0100
+++ b/Program.cpp	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/Program.h	Tue Dec 26 23:46:45 2023 +0100
+++ b/Program.h	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/Shader.cpp	Tue Dec 26 23:46:45 2023 +0100
+++ b/Shader.cpp	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/Shader.h	Tue Dec 26 23:46:45 2023 +0100
+++ b/Shader.h	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/Shark.cpp	Tue Dec 26 23:46:45 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,694 +0,0 @@
-/**
- * ShaderShark
- * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <iostream>
-#include <iomanip>
-#include <string>
-#include <charconv>
-#include <memory>
-#include <functional>
-#include <sstream>
-#include <vector>
-
-#include "x11.h"
-#include "opengl.h"
-#include "EPoll.h"
-#include "Logger.h"
-#include "MappedFile.h"
-#include "ImageLoader.h"
-#include "Texture.h"
-#include "Shader.h"
-#include "Program.h"
-#include "FileMonitor.h"
-#include "XAttrs.h"
-
-#include "Shark.h"
-
-class Shark::Impl {
-public:
-
-	struct {
-		GLint aVertexXYZ = -2;
-		GLint aTextureXY = -2;
-
-		GLint fColor = -2;
-
-		GLint uModel = -2;
-		GLint uView = -2;
-		GLint uProjection = -2;
-		GLint uTexture = -2;
-		GLint uTextureScale = -2;
-	} ProgAttr;
-
-	struct {
-		float yaw = -90.f;
-		float pitch = 0.f;
-		float roll = 0.f;
-		float fov = 45.0f;
-		glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
-		glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
-		glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
-
-		void adjustFov(float diff) {
-			fov += diff;
-			if (fov < 1.0f) fov = 1.0f;
-			else if (fov > 120.0f) fov = 120.0f;
-			std::cerr << "field of view: " << fov << " °" << std::endl;
-		}
-
-		void moveForward(const float cameraSpeed) {
-			cameraPos += cameraSpeed * cameraFront;
-		}
-
-		void moveBackward(const float cameraSpeed) {
-			cameraPos -= cameraSpeed * cameraFront;
-		}
-
-		void moveLeft(const float cameraSpeed) {
-			cameraPos -= glm::normalize(
-					glm::cross(cameraFront, cameraUp)) * cameraSpeed;
-		}
-
-		void moveRight(const float cameraSpeed) {
-			cameraPos += glm::normalize(
-					glm::cross(cameraFront, cameraUp)) * cameraSpeed;
-		}
-
-		void moveUp(const float cameraSpeed) {
-			cameraPos += cameraSpeed * glm::normalize(cameraUp);
-		}
-
-		void moveDown(const float cameraSpeed) {
-			cameraPos -= cameraSpeed * glm::normalize(cameraUp);
-		}
-
-		void updateCameraFrontAndUp() {
-			std::cerr << "--- updateCameraFrontAndUp() --------" << std::endl;
-			dump("pitch, yaw, roll", glm::vec3(pitch, yaw, roll));
-			dump("cameraPos", cameraPos);
-			dump("cameraFront", cameraFront);
-			const auto pitchR = glm::radians(pitch); // around X axis
-			const auto yawR = glm::radians(yaw); //     around Y axis
-			const auto rollR = glm::radians(roll); //   around Z axis
-
-			cameraFront.x = cos(pitchR) * cos(yawR);
-			cameraFront.y = sin(pitchR);
-			cameraFront.z = cos(pitchR) * sin(yawR);
-			cameraFront = glm::normalize(cameraFront);
-			dump("cameraFront", cameraFront);
-			dump("cameraUp", cameraUp);
-
-			// TODO: review ROLL rotation and default angle
-			glm::mat4 rollMatrix = glm::rotate(
-					glm::mat4(1.0f), rollR, cameraFront);
-			cameraUp = glm::mat3(rollMatrix) * glm::vec3(0., 1., 0.);
-			dump("cameraUp", cameraUp);
-			std::cerr << "-------------------------------------" << std::endl;
-		}
-
-		void limitPitch() {
-			if (pitch > +89.0f) pitch = +89.0f;
-			if (pitch < -89.0f) pitch = -89.0f;
-		}
-
-		void turnLeft(const float angleSpeed) {
-			yaw -= angleSpeed;
-			updateCameraFrontAndUp();
-		}
-
-		void turnRight(const float angleSpeed) {
-			yaw += angleSpeed;
-			updateCameraFrontAndUp();
-		}
-
-		void turnUp(const float angleSpeed) {
-			pitch += angleSpeed;
-			limitPitch();
-			updateCameraFrontAndUp();
-		}
-
-		void turnDown(const float angleSpeed) {
-			pitch -= angleSpeed;
-			limitPitch();
-			updateCameraFrontAndUp();
-		}
-
-		void rollLeft(const float angleSpeed) {
-			roll += angleSpeed;
-			updateCameraFrontAndUp();
-		}
-
-		void rollRight(const float angleSpeed) {
-			roll -= angleSpeed;
-			updateCameraFrontAndUp();
-		}
-
-	} initialCtx, ctx;
-
-	Display* dpy;
-	Window win;
-	XVisualInfo* vi;
-	GLXContext glc;
-
-	FileMonitor fileMonitor;
-	std::vector<WatchedFile> watchedFiles;
-	ImageLoader imageLoader;
-	std::vector<std::shared_ptr<Shader>> shaders;
-	std::shared_ptr<Program> shaderProgram;
-	std::vector<std::shared_ptr<Texture>> textures;
-
-	Configuration cfg;
-	std::ostream& logOutput = std::cerr;
-
-	Impl(Configuration cfg) : cfg(cfg) {
-	}
-
-	void run();
-	void clear();
-	void runShaders();
-	Window getRootWindow(Window defaultValue);
-	void log(LogLevel level, std::string message);
-	int setNonBlocking(int fd);
-	void loadVertices();
-	void parametrizeTexture(std::shared_ptr<Texture> tex);
-	bool reloadTexture(const std::string& fileName);
-	void loadTextures();
-	void loadShaders();
-	void updateVariableLocations();
-	bool reloadShader(const std::string& fileName);
-	void setTitle(const std::string& suffix = "");
-	static const std::string getDefaultFile(const std::string& relativePath);
-
-};
-
-Shark::Shark(const Configuration& configuration) :
-impl(new Impl(configuration)) {
-}
-
-Shark::~Shark() {
-	impl->textures.clear();
-	impl->shaders.clear();
-	impl->shaderProgram = nullptr;
-	XFree(impl->vi);
-	glXMakeCurrent(impl->dpy, None, NULL);
-	glXDestroyContext(impl->dpy, impl->glc);
-	XDestroyWindow(impl->dpy, impl->win);
-	XCloseDisplay(impl->dpy);
-	delete impl;
-	// std::cerr << "~Shark()" << std::endl;
-}
-
-void Shark::Impl::setTitle(const std::string& suffix) {
-	std::stringstream title;
-	title << "ShaderShark";
-	if (suffix.size()) title << ": " << suffix.c_str();
-	XStoreName(dpy, win, title.str().c_str());
-	XFlush(dpy);
-}
-
-void Shark::run() {
-	impl->run();
-}
-
-void Shark::Impl::run() {
-	dpy = XOpenDisplay(NULL);
-
-	if (dpy == NULL) throw std::logic_error("Unable to connect to X server");
-
-	GLint att[] = {GLX_RGBA, GLX_DEPTH_SIZE, 24, GLX_DOUBLEBUFFER, None};
-	vi = glXChooseVisual(dpy, 0, att);
-	Window root = DefaultRootWindow(dpy);
-	Window parent = cfg.rootWindow ? cfg.rootWindow : root;
-
-	XSetWindowAttributes swa;
-	swa.colormap = XCreateColormap(dpy, parent, vi->visual, AllocNone);
-	swa.event_mask = ExposureMask | KeyPressMask | PointerMotionMask
-			| ButtonPressMask
-			| StructureNotifyMask;
-
-	bool full = false;
-	unsigned int width = 1600;
-	unsigned int height = 1200;
-	if (parent != root) {
-		XWindowAttributes parentAttr;
-		XGetWindowAttributes(dpy, parent, &parentAttr);
-		width = parentAttr.width;
-		height = parentAttr.height;
-	}
-
-	win = XCreateWindow(
-			dpy, parent, 0, 0, width, height, 0,
-			vi->depth, InputOutput, vi->visual,
-			CWColormap | CWEventMask, &swa);
-
-	XMapWindow(dpy, win);
-	setTitle();
-	setX11PID(dpy, win);
-	// XSetWindowBackground(dpy, win, 0) vs. glClearColor()
-
-	glc = glXCreateContext(dpy, vi, NULL, GL_TRUE);
-	glXMakeCurrent(dpy, win, glc);
-
-	glEnable(GL_DEPTH_TEST);
-	glEnable(GL_BLEND);
-	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
-
-	clear();
-	glXSwapBuffers(dpy, win);
-
-
-	// Load GLSL shaders:
-	loadShaders();
-	loadTextures();
-	loadVertices();
-
-	auto toggleFullscreen = [&]() {
-		full = setFullscreen(dpy, win, !full);
-	};
-
-	auto resetView = [&]() {
-		ctx = initialCtx;
-		ctx.updateCameraFrontAndUp();
-	};
-
-	// root can reize our window
-	// or we can listen to root resize and then resize our window ourselves
-	bool listenToRootResizes = true;
-	if (listenToRootResizes) XSelectInput(dpy, parent, StructureNotifyMask);
-
-	bool keepRunningX11 = true;
-	int x11fd = XConnectionNumber(dpy);
-	EPoll epoll;
-	epoll.add(x11fd);
-	epoll.add(fileMonitor.getFD());
-	try {
-		epoll.add(setNonBlocking(STDIN_FILENO));
-	} catch (const EPoll::Exception& e) {
-		logOutput << "Will not monitor events on STDIN: " << e.what() << "\n";
-	}
-
-	// rended the 3D scene even before the first event:
-	runShaders();
-	glXSwapBuffers(dpy, win);
-
-	for (XEvent xev; keepRunningX11;) {
-		int epollEventCount = epoll.wait();
-		//std::cout << "trace: epoll.wait() = " << epollEventCount << std::endl;
-		for (int epollEvent = 0; epollEvent < epollEventCount; epollEvent++) {
-			bool redraw = false;
-			if (epoll[epollEvent].data.fd == x11fd) {
-				if (!XPending(dpy)) {
-					// otherwise STDIN events are held until the first X11 event
-					logOutput << "trace: no pending X11 event" << std::endl;
-					break;
-				}
-process_x11_event:
-				XWindowAttributes gwa;
-				XNextEvent(dpy, &xev);
-
-				if (xev.type == Expose) {
-					std::cout << "XEvent: Expose" << std::endl;
-					XGetWindowAttributes(dpy, win, &gwa);
-					glViewport(0, 0, gwa.width, gwa.height);
-					redraw = true;
-				} else if (xev.type == KeyPress) {
-					DecodedKey key = decodeKeycode(dpy, xev.xkey.keycode);
-					std::cout << "XEvent: KeyPress:"
-							<< " keycode=" << key.code
-							<< " key=" << key.name
-							<< std::endl;
-
-					const float cSp = 0.05f; // camera speed
-					const float aSp = 5.f; // angle speed
-
-					if (key.matches(XK_q, XK_Escape)) keepRunningX11 = false;
-					else if (key.matches(XK_Left, XK_s)) ctx.turnLeft(aSp);
-					else if (key.matches(XK_Right, XK_f)) ctx.turnRight(aSp);
-					else if (key.matches(XK_Up, XK_e)) ctx.moveForward(cSp);
-					else if (key.matches(XK_Down, XK_d)) ctx.moveBackward(cSp);
-					else if (key.matches(XK_w)) ctx.rollLeft(aSp);
-					else if (key.matches(XK_r)) ctx.rollRight(aSp);
-					else if (key.matches(XK_t)) ctx.turnUp(aSp);
-					else if (key.matches(XK_g)) ctx.turnDown(aSp);
-					else if (key.matches(XK_m)) ctx.moveLeft(cSp);
-					else if (key.matches(XK_comma)) ctx.moveRight(cSp);
-					else if (key.matches(XK_l)) ctx.moveUp(cSp);
-					else if (key.matches(XK_period)) ctx.moveDown(cSp);
-					else if (key.matches(XK_j)) ctx.moveLeft(cSp * 5);
-					else if (key.matches(XK_k)) ctx.moveRight(cSp * 5);
-					else if (key.matches(XK_u)) ctx.moveLeft(cSp * 10);
-					else if (key.matches(XK_i)) ctx.moveRight(cSp * 10);
-					else if (key.matches(XK_x)) resetView();
-					else if (key.matches(XK_F11, XK_y)) toggleFullscreen();
-					redraw = true;
-				} else if (xev.type == ButtonPress) {
-					std::cout << "XEvent: ButtonPress:"
-							<< " button=" << xev.xbutton.button
-							<< std::endl;
-					if (xev.xbutton.button == 1);
-					else if (xev.xbutton.button == 4) ctx.adjustFov(-1.0);
-					else if (xev.xbutton.button == 5) ctx.adjustFov(+1.0);
-					else if (xev.xbutton.button == 8) resetView();
-					else if (xev.xbutton.button == 9) keepRunningX11 = false;
-					redraw = true;
-				} else if (xev.type == MotionNotify) {
-					// printCursorInfo(xev.xmotion);
-				} else if (xev.type == ConfigureNotify) {
-					std::cout << "XEvent: ConfigureNotify:"
-							<< " window=" << xev.xconfigure.window
-							<< " height=" << xev.xconfigure.height
-							<< " width=" << xev.xconfigure.width
-							<< std::endl;
-					if (listenToRootResizes
-							&& xev.xconfigure.window == parent) {
-						XResizeWindow(dpy, win,
-								xev.xconfigure.width, xev.xconfigure.height);
-					}
-				} else if (xev.type == UnmapNotify) {
-					std::cout << "XEvent: UnmapNotify" << std::endl;
-				} else if (xev.type == DestroyNotify) {
-					std::cout << "XEvent: DestroyNotify → finish" << std::endl;
-					break;
-				} else {
-					std::cout << "XEvent: type=" << xev.type << std::endl;
-				}
-				if (XPending(dpy)) goto process_x11_event;
-			} else if (epoll[epollEvent].data.fd == STDIN_FILENO) {
-				int epollFD = epoll[epollEvent].data.fd;
-				logOutput << "other event: fd=" << epollFD << " data=";
-				for (char ch; read(epollFD, &ch, 1) > 0;) {
-					std::stringstream msg;
-					msg
-							<< std::hex
-							<< std::setfill('0')
-							<< std::setw(2)
-							<< (int) ch;
-					logOutput << msg.str();
-				}
-				logOutput << std::endl;
-
-			} else if (epoll[epollEvent].data.fd == fileMonitor.getFD()) {
-				std::cout << "FileMonitor event:" << std::endl;
-				for (FileEvent fe; fileMonitor.readEvent(fe);) {
-					logOutput << "   "
-							<< " file=" << fe.fileName
-							<< " mask=" << fe.mask
-							<< std::endl;
-					try {
-						redraw |= reloadTexture(fe.fileName);
-						redraw |= reloadShader(fe.fileName);
-						setTitle();
-					} catch (const std::exception& e) {
-						setTitle("[ERROR]");
-						logOutput << "error while reloading '"
-								<< fe.fileName.c_str()
-								<< "': " << e.what() << std::endl;
-					}
-				}
-			} else {
-				logOutput
-						<< "error: event on an unexpected FD: "
-						<< epoll[epollEvent].data.fd
-						<< std::endl;
-			}
-
-			if (redraw) {
-				runShaders();
-				glXSwapBuffers(dpy, win);
-			}
-		}
-	}
-}
-
-void Shark::Impl::clear() {
-	glClearColor(
-			(cfg.backgroundColor >> 16 & 0xFF) / 256.,
-			(cfg.backgroundColor >> 8 & 0xFF) / 256.,
-			(cfg.backgroundColor & 0xFF) / 256.,
-			1.0);
-	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
-}
-
-void Shark::Impl::runShaders() {
-	shaderProgram->use();
-	checkError(&std::cerr);
-
-	clear();
-
-	GLint viewport[4];
-	glGetIntegerv(GL_VIEWPORT, viewport);
-	GLfloat width = viewport[2];
-	GLfloat height = viewport[3];
-
-	glm::mat4 projection = glm::perspective(
-			glm::radians(ctx.fov),
-			width / height,
-			0.1f, 100.0f);
-	glUniformMatrix4fv(ProgAttr.uProjection, 1, GL_FALSE, &projection[0][0]);
-
-	glm::mat4 view = glm::lookAt(
-			ctx.cameraPos,
-			ctx.cameraPos + ctx.cameraFront,
-			ctx.cameraUp);
-	glUniformMatrix4fv(ProgAttr.uView, 1, GL_FALSE, &view[0][0]);
-
-	// glBindVertexArray(vao);
-
-	glm::mat4 model = glm::mat4(1.0f); // identity matrix
-	glUniformMatrix4fv(ProgAttr.uModel, 1, GL_FALSE, &model[0][0]);
-
-	// TODO: draw a rectangle for each texture
-	glUniform1f(ProgAttr.uTextureScale, textures[0]->getScale());
-
-	glDrawArrays(GL_TRIANGLES, 0, 2 * 3); // see loadVertices()
-	std::cerr << "GLSL: glDrawArrays()" << std::endl;
-}
-
-void Shark::Impl::log(LogLevel level, std::string message) {
-	::log(logOutput, level, message);
-}
-
-int Shark::Impl::setNonBlocking(int fd) {
-	int flags = fcntl(fd, F_GETFL, 0);
-	fcntl(fd, F_SETFL, flags | O_NONBLOCK);
-	return fd;
-}
-
-void Shark::Impl::loadVertices() {
-	for (int i = 0; i < textures.size(); i++) {
-		std::shared_ptr<Texture> tex = textures[i];
-		// TODO: draw a rectangle for each texture
-		GLfloat ratio = tex->getRatio();
-		const std::vector<GLfloat> vertices = {
-			// Vertex XYZ                      Texture XY
-			-0.80f * ratio, +0.80f, +0.0, /**/ 0.0, 0.0,
-			+0.80f * ratio, +0.80f, +0.0, /**/ 1.0, 0.0,
-			-0.80f * ratio, -0.80f, +0.0, /**/ 0.0, 1.0,
-
-			-0.80f * ratio, -0.80f, +0.0, /**/ 0.0, 1.0,
-			+0.80f * ratio, -0.80f, +0.0, /**/ 1.0, 1.0,
-			+0.80f * ratio, +0.80f, +0.0, /**/ 1.0, 0.0,
-
-			// see glDrawArrays(), where we set start offset and count
-		};
-
-		// Vertex data:
-		glVertexAttribPointer(ProgAttr.aVertexXYZ, 3, // vertex items
-				GL_FLOAT, GL_FALSE, 5 * sizeof (float),
-				(void*) 0);
-		glEnableVertexAttribArray(ProgAttr.aVertexXYZ);
-
-		// Texture positions:
-		glVertexAttribPointer(ProgAttr.aTextureXY, 2, // texture items
-				GL_FLOAT, GL_FALSE, 5 * sizeof (float),
-				(void*) (3 * sizeof (float)));
-		glEnableVertexAttribArray(ProgAttr.aTextureXY);
-
-		glBufferData(GL_ARRAY_BUFFER,
-				vertices.size() * sizeof (vertices[0]),
-				vertices.data(),
-				GL_STATIC_DRAW);
-		// GL_STATIC_DRAW:
-		//   The vertex data will be uploaded once
-		//   and drawn many times(e.g. the world).
-		// GL_DYNAMIC_DRAW:
-		//   The vertex data will be created once, changed from
-		// 	 time to time, but drawn many times more than that.
-		// GL_STREAM_DRAW:
-		//   The vertex data will be uploaded once and drawn once.
-
-		// see also glBindBuffer(GL_ARRAY_BUFFER, vbo); where we set current VBO
-	}
-}
-
-const std::string
-Shark::Impl::getDefaultFile(const std::string& relativePath) {
-	const char* envName = "SHADER_SHARK_DATA_DIR";
-	const char* envValue = ::getenv(envName);
-	if (envValue) {
-		return std::string(envValue) + "/" + relativePath;
-	} else {
-		throw std::invalid_argument(std::string("Configure $") + envName
-				+ " in order to use defaults"
-				" or specify textures and shaders as parameters");
-	}
-}
-
-void Shark::Impl::parametrizeTexture(std::shared_ptr<Texture> tex) {
-	XAttrs xa(tex->getFileName());
-	std::string magf = xa["shader-shark.texture.mag-filter"];
-	std::string minf = xa["shader-shark.texture.min-filter"];
-	std::string scale = xa["shader-shark.texture.scale"];
-
-	auto GLT2D = GL_TEXTURE_2D;
-	auto MAG = GL_TEXTURE_MAG_FILTER;
-	auto MIN = GL_TEXTURE_MIN_FILTER;
-
-	if (magf == "linear") glTexParameteri(GLT2D, MAG, GL_LINEAR);
-	else if (magf == "nearest") glTexParameteri(GLT2D, MAG, GL_NEAREST);
-
-	if (minf == "linear") glTexParameteri(GLT2D, MIN, GL_LINEAR);
-	else if (minf == "nearest") glTexParameteri(GLT2D, MIN, GL_NEAREST);
-
-	if (scale.size()) {
-		float sc;
-		if (std::from_chars(scale.data(), scale.data() + scale.size(), sc).ec
-				== std::errc{}) tex->setScale(sc);
-		else std::cerr << "Invalid texture scale value - expecting float\n";
-		// tex->setScale(std::stof(scale)); // locale-dependent (. vs ,)
-	}
-}
-
-void Shark::Impl::loadTextures() {
-	// Load default texture if there is no configured:
-	if (cfg.textures.empty())
-		cfg.textures.push_back({getDefaultFile("textures/default.png")});
-
-	for (const Configuration::Texture& tex : cfg.textures) {
-		std::shared_ptr<ImageLoader::ImageBuffer>
-				img(imageLoader.loadImage(MappedFile(tex.fileName)));
-		textures.push_back(std::make_shared<Texture>(
-				img->width, img->height, *img, tex.fileName));
-		parametrizeTexture(textures.back());
-		// static const uint32_t watchMask = IN_CLOSE_WRITE | IN_ATTRIB;
-		// watchedFiles.push_back(fileMonitor.watch(tex.fileName, watchMask));
-		watchedFiles.push_back(fileMonitor.watch(tex.fileName));
-		// TODO: review texture loading and binding
-		// works even without this - default texture
-		// glUniform1i(..., ...);
-		// checkError(&std::cerr);
-	}
-}
-
-bool Shark::Impl::reloadTexture(const std::string& fileName) {
-	for (std::shared_ptr<Texture> tex : textures) {
-		if (tex->getFileName() == fileName) {
-			std::shared_ptr<ImageLoader::ImageBuffer>
-					img(imageLoader.loadImage(MappedFile(fileName)));
-			tex->update(img->width, img->height, *img);
-			parametrizeTexture(tex);
-			loadVertices();
-			return true;
-		}
-	}
-	return false;
-}
-
-void Shark::Impl::loadShaders() {
-	// Vertex Array Object (VAO)
-	GLuint vao;
-	glGenVertexArrays(1, &vao);
-	glBindVertexArray(vao);
-	// VAO - something like context for bound data/variables
-	// We can switch multiple VAOs. VAO can contain multiple VBOs.
-	// what-are-vertex-array-objects
-
-	// Vertex Buffer Object (VBO):
-	GLuint vbo;
-	glGenBuffers(1, &vbo);
-	glBindBuffer(GL_ARRAY_BUFFER, vbo);
-
-	{
-		// Load default shaders if there are no configured:
-		int vc = 0;
-		int fc = 0;
-		auto& ss = cfg.shaders;
-		for (const auto& s : ss) if (s.type == "vertex") vc++;
-		for (const auto& s : ss) if (s.type == "fragment") fc++;
-		auto& d = getDefaultFile;
-		if (vc == 0) ss.push_back({d("shaders/default.vert"), "vertex"});
-		if (fc == 0) ss.push_back({d("shaders/default.frag"), "fragment"});
-	}
-
-	shaderProgram = std::make_shared<Program>();
-
-	// glBindFragDataLocation(program, 0, "outColor");
-	// glBindAttribLocation(program, LOC.input, "vertices");
-
-	for (const Configuration::Shader definition : cfg.shaders) {
-		Shader::Type type;
-		std::string fileName = definition.fileName;
-		if (definition.type == "fragment") type = Shader::Type::FRAGMENT;
-		else if (definition.type == "vertex") type = Shader::Type::VERTEX;
-		else throw std::invalid_argument("unsupported shader type");
-
-		MappedFile file(fileName);
-		std::shared_ptr<Shader> shader = std::make_shared<Shader>(
-				type, file, fileName);
-
-		shaderProgram->attachShader(*shader.get());
-		shaders.push_back(shader);
-		watchedFiles.push_back(fileMonitor.watch(fileName));
-		std::cerr << "GLSL loaded: " << fileName.c_str() << std::endl;
-		// We may detach and delete shaders,
-		// but our shaders are small, so we keep them for later reloading.
-	}
-
-	shaderProgram->link();
-	updateVariableLocations();
-	// listVariables(program);
-	std::cerr << "GLSL shader count: " << shaders.size() << std::endl;
-}
-
-void Shark::Impl::updateVariableLocations() {
-	// GLSL compiler does very efficient / aggressive optimization.
-	// Attributes and uniforms that are not used in the shader are deleted.
-	// And even if we e.g. read color from a texture and overwrite it,
-	// the variable is still deleted and considered „inactive“.
-	// Functions glGetAttribLocation() and glGetUniformLocation() return -1.
-	ProgAttr.aVertexXYZ = shaderProgram->getAttribLocation("aVertexXYZ");
-	ProgAttr.aTextureXY = shaderProgram->getAttribLocation("aTextureXY");
-	ProgAttr.uModel = shaderProgram->getUniformLocation("uModel");
-	ProgAttr.uView = shaderProgram->getUniformLocation("uView");
-	ProgAttr.uProjection = shaderProgram->getUniformLocation("uProjection");
-	ProgAttr.uTexture = shaderProgram->getUniformLocation("uTexture");
-	ProgAttr.uTextureScale = shaderProgram->getUniformLocation("uTextureScale");
-	ProgAttr.fColor = shaderProgram->getFragDataLocation("fColor");
-	shaderProgram->bindFragDataLocation("fColor", ProgAttr.fColor);
-}
-
-bool Shark::Impl::reloadShader(const std::string& fileName) {
-	for (auto shader : shaders) {
-		if (shader->getFileName() == fileName) {
-			shader->update(MappedFile(fileName));
-			shaderProgram->link();
-			updateVariableLocations();
-			return true;
-		}
-	}
-	return false;
-}
--- a/Shark.h	Tue Dec 26 23:46:45 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,32 +0,0 @@
-/**
- * ShaderShark
- * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include "Configuration.h"
-
-class Shark {
-public:
-	Shark(const Configuration& cfg);
-	virtual ~Shark();
-	void run();
-private:
-	class Impl;
-	Impl* impl;
-	Shark(const Shark&) = delete;
-	Shark& operator=(const Shark&) = delete;
-};
--- a/Texture.cpp	Tue Dec 26 23:46:45 2023 +0100
+++ b/Texture.cpp	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/Texture.h	Tue Dec 26 23:46:45 2023 +0100
+++ b/Texture.h	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/XAttrs.cpp	Tue Dec 26 23:46:45 2023 +0100
+++ b/XAttrs.cpp	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/XAttrs.h	Tue Dec 26 23:46:45 2023 +0100
+++ b/XAttrs.h	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/bash-completion.sh	Tue Dec 26 23:46:45 2023 +0100
+++ b/bash-completion.sh	Wed Dec 27 01:50:38 2023 +0100
@@ -1,4 +1,4 @@
-# ShaderShark
+# OHP3D
 # Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
 #
 # This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
-_shader_shark_completion() {
+_ohp3d_completion() {
 	local w0 w1 w2 w3
 
 	COMPREPLY=()
@@ -52,4 +52,4 @@
 	fi
 }
 
-complete -F _shader_shark_completion shader-shark
+complete -F _ohp3d_completion ohp3d
--- a/nbproject/configurations.xml	Tue Dec 26 23:46:45 2023 +0100
+++ b/nbproject/configurations.xml	Wed Dec 27 01:50:38 2023 +0100
@@ -6,10 +6,10 @@
       <in>ImageLoader.cpp</in>
       <in>Program.cpp</in>
       <in>Shader.cpp</in>
-      <in>Shark.cpp</in>
+      <in>OHP3D.cpp</in>
       <in>Texture.cpp</in>
       <in>XAttrs.cpp</in>
-      <in>shader-shark.cpp</in>
+      <in>ohp3d.cpp</in>
     </df>
     <logicalFolder name="ExternalFiles"
                    displayName="Important Files"
@@ -41,7 +41,7 @@
           <buildCommandWorkingDir>.</buildCommandWorkingDir>
           <buildCommand>${MAKE} -f Makefile</buildCommand>
           <cleanCommand>${MAKE} -f Makefile clean</cleanCommand>
-          <executablePath>build/shader-shark</executablePath>
+          <executablePath>build/ohp3d</executablePath>
           <ccTool>
             <incDir>
               <pElem>/usr/include/x86_64-linux-gnu/ImageMagick-6</pElem>
@@ -74,7 +74,7 @@
         <ccTool flags="0">
         </ccTool>
       </item>
-      <item path="Shark.cpp" ex="false" tool="1" flavor2="0">
+      <item path="OHP3D.cpp" ex="false" tool="1" flavor2="0">
         <ccTool flags="0">
         </ccTool>
       </item>
@@ -86,7 +86,7 @@
         <ccTool flags="0">
         </ccTool>
       </item>
-      <item path="shader-shark.cpp" ex="false" tool="1" flavor2="0">
+      <item path="ohp3d.cpp" ex="false" tool="1" flavor2="0">
         <ccTool flags="0">
         </ccTool>
       </item>
--- a/nbproject/project.xml	Tue Dec 26 23:46:45 2023 +0100
+++ b/nbproject/project.xml	Wed Dec 27 01:50:38 2023 +0100
@@ -3,7 +3,7 @@
     <type>org.netbeans.modules.cnd.makeproject</type>
     <configuration>
         <data xmlns="http://www.netbeans.org/ns/make-project/1">
-            <name>ShaderShark</name>
+            <name>OHP3D</name>
             <c-extensions/>
             <cpp-extensions>cpp</cpp-extensions>
             <header-extensions>h</header-extensions>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ohp3d.cpp	Wed Dec 27 01:50:38 2023 +0100
@@ -0,0 +1,50 @@
+/**
+ * OHP3D
+ * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <iostream>
+#include <locale>
+#include <stdexcept>
+#include <vector>
+#include <string>
+
+#include "CLIParser.h"
+#include "OHP3D.h"
+
+int main(int argc, char** argv) {
+	setlocale(LC_ALL, "");
+	std::cout << "OHP3D for OpenGL" << std::endl;
+	int result = 0;
+
+	try {
+		std::vector<std::string> arguments;
+		for (int i = 1; i < argc; i++) arguments.push_back(argv[i]);
+
+		CLIParser cliParser;
+		Configuration cfg = cliParser.parse(arguments);
+
+		OHP3D shaderOHP3D(cfg);
+		shaderOHP3D.run();
+	} catch (const std::exception& e) {
+		std::cerr << "Exception: " << e.what() << std::endl;
+		result = 1;
+	} catch (...) {
+		std::cerr << "Exception: unexpected" << std::endl;
+		result = 2;
+	}
+
+	return result;
+}
--- a/opengl.h	Tue Dec 26 23:46:45 2023 +0100
+++ b/opengl.h	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
--- a/shader-shark.cpp	Tue Dec 26 23:46:45 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-/**
- * ShaderShark
- * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <iostream>
-#include <locale>
-#include <stdexcept>
-#include <vector>
-#include <string>
-
-#include "CLIParser.h"
-#include "Shark.h"
-
-int main(int argc, char** argv) {
-	setlocale(LC_ALL, "");
-	std::cout << "ShaderShark for OpenGL" << std::endl;
-	int result = 0;
-
-	try {
-		std::vector<std::string> arguments;
-		for (int i = 1; i < argc; i++) arguments.push_back(argv[i]);
-
-		CLIParser cliParser;
-		Configuration cfg = cliParser.parse(arguments);
-
-		Shark shaderShark(cfg);
-		shaderShark.run();
-	} catch (const std::exception& e) {
-		std::cerr << "Exception: " << e.what() << std::endl;
-		result = 1;
-	} catch (...) {
-		std::cerr << "Exception: unexpected" << std::endl;
-		result = 2;
-	}
-
-	return result;
-}
Binary file textures/default.png has changed
--- a/x11.h	Tue Dec 26 23:46:45 2023 +0100
+++ b/x11.h	Wed Dec 27 01:50:38 2023 +0100
@@ -1,5 +1,5 @@
 /**
- * ShaderShark
+ * OHP3D
  * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify