OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / gpu / glsl_emit_context.cpp

214 lines · cpp ref:
#include "glsl_emit_context.h"

#include "../graph/proc_node.h"
#include "../graph/proc_node_graph.h"

#include <godot_cpp/variant/utility_functions.hpp>

namespace godot {

String GlslEmitContext::type_name(SocketType p_type) {
	switch (p_type) {
		case SocketType::Float: return "float";
		case SocketType::Int: return "int";
		case SocketType::Bool: return "bool";
		case SocketType::Vector: return "vec3";
		case SocketType::Color: return "vec4";
		case SocketType::Rotation: return "vec4";
		case SocketType::Matrix: return "mat4";
		// String and Geometry deliberately have no spelling.
		default: return String();
	}
}

String GlslEmitContext::literal(int p_node_idx, int p_socket) const {
	if (graph == nullptr) {
		return String();
	}
	Ref<ProcNode> node = graph->get_node(p_node_idx);
	if (node.is_null() || p_socket < 0 || p_socket >= node->get_input_count()) {
		return String();
	}
	return literal_value(node->get_input_def(p_socket).type, node->get_input_default(p_socket));
}

String GlslEmitContext::literal_value(SocketType p_type, const Variant &v) const {
	switch (p_type) {
		case SocketType::Float:
			return String::num((double)v, 9);
		case SocketType::Int: {
			const int i = (int)v;
			// The octave clamp is the reason integers are not simply printed:
			// a graph evaluated at a coarse LOD must not run more octaves than
			// the voxel size can carry, and the consumer supplies the limit.
			return clamp_octaves ? (String("int(min(") + itos(i) + ", pn_max_octaves))") : itos(i);
		}
		case SocketType::Bool:
			return ((bool)v) ? "true" : "false";
		case SocketType::Vector: {
			const Vector3 vec = v;
			return String("vec3(") + String::num(vec.x, 9) + ", " + String::num(vec.y, 9) + ", " +
					String::num(vec.z, 9) + ")";
		}
		case SocketType::Color: {
			const Color c = v;
			return String("vec4(") + String::num(c.r, 9) + ", " + String::num(c.g, 9) + ", " +
					String::num(c.b, 9) + ", " + String::num(c.a, 9) + ")";
		}
		default:
			return String();
	}
}

String GlslEmitContext::param(int p_node_idx, int p_socket) const {
	if (!push_constants || graph == nullptr) {
		return in(p_node_idx, p_socket);
	}
	// ⛔ A PUSH CONSTANT IS ONE VALUE FOR EVERY VERTEX, so a socket driven by a
	// per-vertex producer must not read one. When that producer is emitted into
	// this same shader its result is a local, and reading the local is both
	// correct and free. When it is NOT -- a single-node batch fed from outside
	// -- the push constant is all there is, and the executor refuses the batch
	// if the value behind it is a field.
	const int src = graph->get_input_source_node(p_node_idx, p_socket);
	if (src >= 0 && is_emitted(src)) {
		return in(p_node_idx, p_socket);
	}
	Ref<ProcNode> node = graph->get_node(p_node_idx);
	if (node.is_null()) {
		return in(p_node_idx, p_socket);
	}
	const PackedInt32Array map = node->get_push_constant_sockets();

	// Walk the map summing widths. A Vector takes three slots; everything else
	// takes one. This is the same walk GraphExecutor does when it packs, and
	// deliberately so -- a second spelling of it is the drift this ends.
	int slot = 0;
	for (int i = 0; i < map.size(); i++) {
		const int sock = map[i];
		const int width = (node->get_input_type(sock) == (int)SocketType::Vector) ? 3 : 1;
		if (sock == p_socket) {
			// The block is element_count + seven floats. A map that overruns it
			// would silently read a neighbour's slot, so say so and fall back to
			// the literal rather than emit a wrong address.
			if (slot + width > 7) {
				UtilityFunctions::push_error(
						"proc-nodes: node ", p_node_idx, " socket ", p_socket,
						" is declared at push-constant slot ", slot, " width ", width,
						", past the seven-float block. Emitting its default instead.");
				return in(p_node_idx, p_socket);
			}
			if (width == 3) {
				return String("vec3(pc.param") + itos(slot) + ", pc.param" + itos(slot + 1) +
						", pc.param" + itos(slot + 2) + ")";
			}
			return String("pc.param") + itos(slot);
		}
		slot += width;
	}
	// Not declared: an ordinary input.
	return in(p_node_idx, p_socket);
}

String GlslEmitContext::out(int p_node_idx, int p_socket) const {
	return String("n") + itos(p_node_idx) + "_s" + itos(p_socket);
}

String GlslEmitContext::declare(int p_node_idx, int p_socket) const {
	if (graph == nullptr) {
		return String();
	}
	Ref<ProcNode> node = graph->get_node(p_node_idx);
	if (node.is_null() || p_socket < 0 || p_socket >= node->get_output_count()) {
		return String();
	}
	const String t = type_name(node->get_output_def(p_socket).type);
	if (t.is_empty()) {
		return String();
	}
	return t + String(" ") + out(p_node_idx, p_socket);
}

String GlslEmitContext::in(int p_node_idx, int p_socket) const {
	if (graph == nullptr) {
		// An empty expression splices into the shader as a syntax error at best
		// and a silently different field at worst, which is the failure this
		// context exists to end. Say so.
		UtilityFunctions::push_error(
				"GlslEmitContext::in() called without a graph -- node ", p_node_idx,
				" socket ", p_socket, ". The emitter cannot resolve an input.");
		return String();
	}
	const int src_node = graph->get_input_source_node(p_node_idx, p_socket);
	if (src_node < 0) {
		return literal(p_node_idx, p_socket);
	}
	// ⛔ A LOCAL ONLY EXISTS IF ITS PRODUCER IS IN THIS SHADER.
	//
	// param() learned this; in() did not, and nodes that read their inputs
	// directly -- Combine XYZ among them -- go straight through here. The batch
	// for the bridged mountain held nodes 31 and 32 only, and Combine XYZ still
	// emitted `vec3 n31_s0 = vec3(0.0, 0.0, n30_s0);` for a producer that was
	// never written:
	//
	//     ERROR: 0:46: 'n30_s0' : undeclared identifier
	//
	// Scoped to the BATCH path by push_constants. compile_to_function emits the
	// whole graph and sets no emitted_nodes at all, so checking there would
	// reduce every input to its default and silently gut the density shader.
	if (push_constants && !is_emitted(src_node)) {
		return literal(p_node_idx, p_socket);
	}
	const int src_socket = graph->get_input_source_socket(p_node_idx, p_socket);
	const String expr = out(src_node, src_socket);

	Ref<ProcNode> dst = graph->get_node(p_node_idx);
	Ref<ProcNode> src = graph->get_node(src_node);
	if (dst.is_null() || src.is_null()) {
		return expr;
	}
	if (p_socket >= dst->get_input_count() || src_socket >= src->get_output_count()) {
		return expr;
	}
	const SocketType want = dst->get_input_def(p_socket).type;
	const SocketType have = src->get_output_def(src_socket).type;
	if (want == have) {
		return expr;
	}

	// COERCIONS MATCH THE CPU, NOT BLENDER. SocketValue::to_float() takes vec.x
	// from a vector where Blender AVERAGES x, y and z, and a float read as a
	// vector broadcasts. The emitter reproduces the CPU so that the two paths
	// agree with each other; the asymmetry with Blender is a real defect, and
	// hiding it inside a coercion here is how it would stop being findable.
	switch (want) {
		case SocketType::Float:
			if (have == SocketType::Vector) return expr + String(".x");
			if (have == SocketType::Color) return expr + String(".r");
			if (have == SocketType::Int || have == SocketType::Bool) return String("float(") + expr + ")";
			break;
		case SocketType::Int:
			if (have == SocketType::Float) return String("int(") + expr + ")";
			if (have == SocketType::Bool) return String("int(") + expr + ")";
			break;
		case SocketType::Bool:
			if (have == SocketType::Float) return String("(") + expr + " != 0.0)";
			if (have == SocketType::Int) return String("(") + expr + " != 0)";
			break;
		case SocketType::Vector:
			if (have == SocketType::Float) return String("vec3(") + expr + ")";
			if (have == SocketType::Int) return String("vec3(float(") + expr + "))";
			if (have == SocketType::Color) return expr + String(".rgb");
			break;
		case SocketType::Color:
			if (have == SocketType::Float) return String("vec4(vec3(") + expr + "), 1.0)";
			if (have == SocketType::Vector) return String("vec4(") + expr + ", 1.0)";
			break;
		default:
			break;
	}
	return expr;
}

} // namespace godot