Silnith’s Lair — LiveJournal
Jan. 11th, 2024
11:52 am - API Design
I would like to analyze what makes a programming API good versus bad. I feel like a lot of new APIs lack wisdom and foresight. A good programming API should guide the programmer away from making mistakes and towards using the API correctly and efficiently. But it seems that a lot of APIs are designed by people who only think of how they personally would use the API, while neglecting to consider how others may perceive it. Towards this end I have spent the past couple months getting back into my graduate school studies of computer graphics, in order to compare an API I consider to be one of the finest ever created, namely OpenGL 1.x, against an API I consider to be truly terrible, namely OpenGL 3.x.
I have implemented a simple screensaver using different versions of the OpenGL API. It consists of a bunch of solid-color squares that spin around an invisible axis. The point is not to make anything fancy, it is simply to exercise a theoretical 3D graphics pipeline in its purest form.
First, a few ground rules. The rendered output for all versions is absolutely identical. There is no variation in where or how computations are performed except where forced by fundamental changes in the OpenGL API itself. The most important consequence of this is that I purposely ignore the pervasive online advice and tutorials that all urge programmers to perform vertex transformations on the CPU; the original version 1.0 of the OpenGL API was designed to facilitate handling all vertex transformations by any available hardware, so I have maintained that discipline when rewriting my program to work on the newer versions of the API.
Second, because the original version 1.0 of the OpenGL API included a highly versatile "call list" feature that allowed pre-compiling sequences of state changes, transformations, and rendering commands and storing the result directly in the "server" high-speed graphics memory, I have used the transform feedback mechanism to emulate what a high-quality implementation of the original API would have been able to achieve. Not all implementations provided such sophisticated support, but the API was designed with the intention that such offerings could and would appear and so my rewrite stays consistent with that capability.
Finally, I have used the OpenGL API as it was designed to be used, not as it is most popularly used. The most prominent example of this is how in the 3.2 version I explicitly query the uniform offsets for the uniform block defined in the shaders, rather than specify the compatibility layout qualifier. The intent of allowing the OpenGL implementation to compile shaders rather than be forced to accept a pre-defined intermediate form is so that any OpenGL implementation is free to use whatever optimizations it prefers, including unique memory layout and padding. Therefore my program makes no assumptions about any of these details, it simply complies with whatever the OpenGL implementation provides. This applies to vertex attribute locations, uniform locations, and uniform block offsets. A much less significant example is how I allocate my call lists in the 1.1 version of the program, even though according to the specification it is not strictly necessary.
Here are the two versions of the screensaver, with all extraneous code removed to leave only the direct uses of OpenGL itself. (The GitHub repository contains the full source code.)
OpenGL 1.1 version
Global initialization.
glGetString(GL_VERSION);
glEnable(GL_DEPTH_TEST);
glPolygonOffset(-0.5, -2);
glEnable(GL_POLYGON_OFFSET_LINE);
glLoadIdentity();
gluLookAt(0, 50, 50,
0, 0, 13,
0, 0, 1);
wingDisplayList = glGenLists(1);
glNewList(wingDisplayList, GL_COMPILE);
glBegin(GL_QUADS);
glVertex2f(1, 1);
glVertex2f(-1, 1);
glVertex2f(-1, -1);
glVertex2f(1, -1);
glEnd();
glEndList();
rotatingWingDisplayLists = glGenLists(numWings);
// Executed when the window changes size.
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(-26.6, 26.6, -20, 20, 35, 105);
glMatrixMode(GL_MODELVIEW);
Per-frame changes.
glNewList(displayList, GL_COMPILE);
glPushMatrix();
glRotatef(angle, 0, 0, 1);
glTranslatef(radius, 0, 0);
glRotatef(-yaw, 0, 0, 1);
glRotatef(-pitch, 0, 1, 0);
glRotatef(roll, 1, 0, 0);
glCallList(wingDisplayList);
glPopMatrix();
glEndList();
Drawing code.
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glPushMatrix();
for (auto wing : wings) {
glTranslatef(0, 0, deltaZ);
glRotatef(deltaAngle, 0, 0, 1);
glColor3f(red, green, blue);
glCallList(displayList);
}
glPopMatrix();
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glPushMatrix();
for (auto wing : wings) {
glTranslatef(0, 0, deltaZ);
glRotatef(deltaAngle, 0, 0, 1);
glColor3f(red, green, blue);
glCallList(displayList);
}
glPopMatrix();
glFlush();
Analysis
I believe the most prominent feature of this code is how virtually every API call is a direct expression of a concept that is well-known to people who have studied 3D graphics. All graphics programmers understand what a depth test is, because it is an intrinsic part of a 3D pipeline. No matter what programming API or hardware one decides to use, the 3D graphics code will still make use of linear algebra to accomplish rotations and translations expressed as matrices. The data to render will always be represented as polygons specified by vertices. Really the only concept that might be foreign to some would be the concept of a "call list", but even that is a simple abstraction of "achieve the same result as if this sequence of other API calls had been made." (Some make the mistake of assuming that means the implementation must issue an exact duplicate of the sequences of API calls, but the OpenGL implementation is only required to produce the same result as if that sequence had been called. Hence why creating a call list uses the term "compilation".)
Some may look at the drawing code and complain that all of the transformations are "slow" because they change the state of the modelview matrix. This would be a misunderstanding of the concept of a 3D graphics pipeline. The 3D pipeline was so-named because state changes like vertex colors and modelview transformations are designed to be pipelined. The reason that the drawing code iterates the list of wings twice is that changing the rendering mode from polygon fill to outline and back is much more likely to trigger a pipeline flush than simply modifying the transformation matrices, because that replaced one rasterization algorithm with another. At least, that is how 3D hardware worked before it was all scrapped and replaced with generic stream processors. Which leads us to the next version of the code.
OpenGL 3.2 version
Global initialization.
GLfloat const quadVertices[16]{ 1,1,-1,1,-1,-1,1,-1, };
GLuint const quadIndices[4]{ 0,1,2,3 };
GLfloat const identity[16]{ 1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1, };
glGetIntegerv(GL_MAJOR_VERSION, &glMajorVersion);
glGetIntegerv(GL_MINOR_VERSION, &glMinorVersion);
glEnable(GL_DEPTH_TEST);
glPolygonOffset(0.5, 2);
glEnable(GL_POLYGON_OFFSET_FILL);
glGenBuffers(3 + 3 * numWings, bufferNames);
glGenVertexArrays(2, vertexArrayNames);
glBindBuffer(GL_ARRAY_BUFFER, originalVertexBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, wingIndexBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(quadIndices), quadIndices, GL_STATIC_DRAW);
GLuint feedbackVertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(feedbackVertexShader, 1, source, nullptr);
glCompileShader(feedbackVertexShader);
glGetShaderiv(feedbackVertexShader, GL_INFO_LOG_LENGTH, &logSize);
glGetShaderInfoLog(feedbackVertexShader, logSize, nullptr, log);
glGetShaderiv(feedbackVertexShader, GL_COMPILE_STATUS, &compilationSuccess);
GLuint feedbackProgram = glCreateProgram();
GLchar const* varyings[3]{ "gl_Position","varyingWingColor","varyingEdgeColor", };
glTransformFeedbackVaryings(feedbackProgram, 3, varyings, GL_SEPARATE_ATTRIBS);
glAttachShader(feedbackProgram, feedbackVertexShader);
glLinkProgram(feedbackProgram);
glDetachShader(feedbackProgram, feedbackVertexShader);
glGetProgramiv(feedbackProgram, GL_INFO_LOG_LENGTH, &logSize);
glGetProgramInfoLog(feedbackProgram, logSize, nullptr, log);
glGetProgramiv(feedbackProgram, GL_LINK_STATUS, &linkSuccess);
glDeleteShader(feedbackVertexShader);
glBindVertexArray(wingTransformVertexArray);
vertexAttribLocation = glGetAttribLocation(feedbackProgram, "vertex");
glEnableVertexAttribArray(vertexAttribLocation);
GLuint renderVertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(renderVertexShader, 1, source, nullptr);
glCompileShader(renderVertexShader);
glGetShaderiv(renderVertexShader, GL_INFO_LOG_LENGTH, &logSize);
glGetShaderInfoLog(renderVertexShader, logSize, nullptr, log);
glGetShaderiv(renderVertexShader, GL_COMPILE_STATUS, &compilationSuccess);
GLuint renderFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(renderFragmentShader, 1, source, nullptr);
glCompileShader(renderFragmentShader);
glGetShaderiv(renderFragmentShader, GL_INFO_LOG_LENGTH, &logSize);
glGetShaderInfoLog(renderFragmentShader, logSize, nullptr, log);
glGetShaderiv(renderFragmentShader, GL_COMPILE_STATUS, &compilationSuccess);
GLuint renderProgram = glCreateProgram();
glBindFragDataLocation(renderProgram, 0, "fragmentColor");
glAttachShader(renderProgram, renderVertexShader);
glAttachShader(renderProgram, renderFragmentShader);
glLinkProgram(renderProgram);
glDetachShader(renderProgram, renderFragmentShader);
glDetachShader(renderProgram, renderVertexShader);
glGetProgramiv(renderProgram, GL_INFO_LOG_LENGTH, &logSize);
glGetProgramInfoLog(renderProgram, logSize, nullptr, log);
glGetProgramiv(renderProgram, GL_LINK_STATUS, &linkSuccess);
glDeleteShader(renderVertexShader);
glDeleteShader(renderFragmentShader);
glBindVertexArray(renderVertexArray);
vertexAttribLocation = glGetAttribLocation(renderProgram, "vertex");
colorAttribLocation = glGetAttribLocation(renderProgram, "color");
glEnableVertexAttribArray(vertexAttribLocation);
glEnableVertexAttribArray(colorAttribLocation);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, wingIndexBuffer);
GLuint blockIndex = glGetUniformBlockIndex(renderProgram, "ModelViewProjection");
glUniformBlockBinding(renderProgram, blockIndex, modelViewProjectionBindingIndex);
GLchar const* names[3]{ "model", "view", "projection" };
glGetUniformIndices(renderProgram, 3, names, uniformIndices);
glGetActiveUniformsiv(renderProgram, 3, uniformIndices, GL_UNIFORM_OFFSET, uniformOffsets);
glGetActiveUniformBlockiv(renderProgram, blockIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &modelViewProjectionUniformDataSize);
glBindBufferBase(GL_UNIFORM_BUFFER, modelViewProjectionBindingIndex, modelViewProjectionUniformBuffer);
glBindBuffer(GL_UNIFORM_BUFFER, modelViewProjectionUniformBuffer);
glBufferData(GL_UNIFORM_BUFFER, modelViewProjectionUniformDataSize, nullptr, GL_STATIC_DRAW);
glBufferSubData(GL_UNIFORM_BUFFER, uniformOffsets[0], sizeof(identity), identity);
GLfloat const view[16]{ /* implement your own gluLookAt */ };
glBufferSubData(GL_UNIFORM_BUFFER, uniformOffsets[1], sizeof(view), view);
for (auto wing : wings) {
glBindBuffer(GL_ARRAY_BUFFER, wingVertexBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 4 * numVertices, nullptr, GL_STREAM_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, wingColorBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 3 * numVertices, nullptr, GL_STREAM_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, wingEdgeColorBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 3 * numVertices, nullptr, GL_STREAM_DRAW);
}
// Executed when the window changes size.
glViewport(0, 0, width, height);
GLfloat const projection[16]{ /* implement your own glOrtho */ };
glBindBuffer(GL_UNIFORM_BUFFER, modelViewProjectionUniformBuffer);
glBufferSubData(GL_UNIFORM_BUFFER, uniformOffsets[2], sizeof(projection), projection);
Per-frame vertex shader for transform feedback.
#version 150
uniform vec2 radiusAngle;
uniform vec3 rollPitchYaw;
uniform vec3 color;
uniform vec3 edgeColor = vec3(1, 1, 1);
in vec4 vertex;
smooth out vec3 varyingWingColor;
smooth out vec3 varyingEdgeColor;
mat4 rotate(in float angle, in vec3 axis) {
float c = cos(radians(angle));
float s = sin(radians(angle));
mat3 initial = outerProduct(axis, axis)
* (1 - c);
mat3 c_part = mat3(c);
mat3 s_part = mat3(0, axis.z, -axis.y,
-axis.z, 0, axis.x,
axis.y, -axis.x, 0)
* s;
mat3 temp = initial + c_part + s_part;
mat4 rotation = mat4(1.0);
rotation[0].xyz = temp[0];
rotation[1].xyz = temp[1];
rotation[2].xyz = temp[2];
return rotation;
}
mat4 translate(in vec3 move) {
mat4 trans = mat4(1.0);
trans[3].xyz = move;
return trans;
}
const vec3 xAxis = vec3(1, 0, 0);
const vec3 yAxis = vec3(0, 1, 0);
const vec3 zAxis = vec3(0, 0, 1);
void main() {
float radius = radiusAngle[0];
float angle = radiusAngle[1];
float roll = rollPitchYaw[0];
float pitch = rollPitchYaw[1];
float yaw = rollPitchYaw[2];
varyingWingColor = color;
varyingEdgeColor = edgeColor;
mat4 wingTransformation = rotate(angle, zAxis)
* translate(vec3(radius, 0, 0))
* rotate(-yaw, zAxis)
* rotate(-pitch, yAxis)
* rotate(roll, xAxis);
gl_Position = wingTransformation * vertex;
}
Per-frame changes.
GLint radiusAngleUniformLocation = glGetUniformLocation(transformProgram, "radiusAngle");
GLint rollPitchYawUniformLocation = glGetUniformLocation(transformProgram, "rollPitchYaw");
GLint colorUniformLocation = glGetUniformLocation(transformProgram, "color");
GLint edgeColorUniformLocation = glGetUniformLocation(transformProgram, "edgeColor");
GLuint vertexAttributeLocation = glGetAttribLocation(transformProgram, "vertex");
glUseProgram(transformProgram);
glUniform2f(radiusAngleUniformLocation, radius, angle);
glUniform3f(rollPitchYawUniformLocation, roll, pitch, yaw);
glUniform3f(colorUniformLocation, red, green, blue);
glUniform3f(edgeColorUniformLocation, edgeRed, edgeGreen, edgeBlue);
glBindVertexArray(wingTransformVertexArray);
glBindBuffer(GL_ARRAY_BUFFER, originalVertexBuffer);
glVertexAttribPointer(vertexAttributeLocation, 2, GL_FLOAT, GL_FALSE, 0, 0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, wingVertexBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, wingColorBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 2, wingEdgeColorBuffer);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, numVertices);
glEndTransformFeedback();
Drawing vertex shader.
#version 150
uniform ModelViewProjection {
mat4 model;
mat4 view;
mat4 projection;
};
uniform vec2 deltaZ = vec2(15, 0.5);
in vec4 vertex;
in vec4 color;
smooth out vec4 varyingColor;
mat4 rotate(in float angle, in vec3 axis) {
float c = cos(radians(angle));
float s = sin(radians(angle));
mat3 initial = outerProduct(axis, axis)
* (1 - c);
mat3 c_part = mat3(c);
mat3 s_part = mat3(0, axis.z, -axis.y,
-axis.z, 0, axis.x,
axis.y, -axis.x, 0)
* s;
mat3 temp = initial + c_part + s_part;
mat4 rotation = mat4(1.0);
rotation[0].xyz = temp[0];
rotation[1].xyz = temp[1];
rotation[2].xyz = temp[2];
return rotation;
}
mat4 translate(in vec3 move) {
mat4 trans = mat4(1.0);
trans[3].xyz = move;
return trans;
}
const vec3 xAxis = vec3(1, 0, 0);
const vec3 yAxis = vec3(0, 1, 0);
const vec3 zAxis = vec3(0, 0, 1);
void main() {
float deltaAngle = deltaZ[0];
float deltaZ = deltaZ[1];
mat4 modelViewProjection = projection * view * model;
varyingColor = color;
gl_Position = modelViewProjection
* translate(vec3(0, 0, deltaZ))
* rotate(deltaAngle, zAxis)
* vertex;
}
Drawing fragment shader.
#version 150
smooth in vec4 varyingColor;
out vec4 fragmentColor;
void main() {
fragmentColor = varyingColor;
}
Drawing code.
GLint deltaZUniformLocation = glGetUniformLocation(renderProgram, "deltaZ");
GLuint vertexAttribLocation = glGetAttribLocation(renderProgram, "vertex");
GLuint colorAttribLocation = glGetAttribLocation(renderProgram, "color");
glUseProgram(renderProgram);
glBindVertexArray(renderVertexArray);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
for (auto wing : wings) {
glUniform2f(deltaZUniformLocation, deltaAngle, deltaZ);
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
glVertexAttribPointer(vertexAttribLocation, 4, GL_FLOAT, GL_FALSE, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, edgeColorBuffer);
glVertexAttribPointer(colorAttribLocation, 3, GL_FLOAT, GL_FALSE, 0, 0);
glDrawElements(GL_LINE_LOOP, numIndices, GL_UNSIGNED_INT, 0);
glBindBuffer(GL_ARRAY_BUFFER, colorBuffer);
glVertexAttribPointer(colorAttribLocation, 3, GL_FLOAT, GL_FALSE, 0, 0);
glDrawElements(GL_TRIANGLE_FAN, numIndices, GL_UNSIGNED_INT, 0);
}
glFlush();
Analysis
What strikes me the most about this code is how little of it actually has to do with rendering 3D graphics. Instead, it is almost entirely devoted to allocating and managing memory buffers. What little remains in it of actual 3D graphics and linear algebra is mostly places where standard functionality that used to be provided by the API is instead forced back onto the programmer. All of the matrix transformations must be re-implemented in every program instead of one single, shared, optimized implementation being provided. And as I had previously indicated, this has resulted in the overwhelming majority of educational material repeating the terrible mistake of moving these calculations away from the specialized hardware designed for efficient matrix computations and instead dumping them back on the general-purpose CPU with its slow shared memory.
But in my mind this is not actually the primary sin of version 3.2 of the OpenGL API. No, that sin is the sheer number of opportunities that the "updated" version of the API provides for the programmer to make terrible mistakes. As I pointed out, the majority of the API calls in this "newer" version of the API are for managing memory buffers. That means we now have to deal with all of the potential problems and pitfalls inherent in memory management. We have uninitialized memory, we have buffer overruns, we have data format mismatches, and we have to deal with all of these problems in an environment where we (by design) cannot even guarantee we can view the memory we are managing in order to debug our mistakes! Meanwhile, back in OpenGL 1.0 land, the API rigidly constrained the structure and format of data that we could pass in. When writing the OpenGL 1.1 version of the screensaver, the worst problem I had to deal with was polygons appearing in the wrong position on screen or possibly not at all. When writing the OpenGL 3.2 version, I had nasty crashes where the video driver would segfault, not even giving me a stack trace into my own program.
The other thing that strikes me about the 3.2 version is that, in exchange for all of the massive amounts of initialization done at the start, I had expected that the per-frame code and drawing code would be significantly shorter than the 1.1 version. But upon review, they really are not much shorter at all, and looking at the actual calls it really feels like they should be more expensive to execute, rather than less.
Originally I had intended to go through each API call in both versions, one by one, and identify exactly what mistakes each call allowed the programmer to make as well as explain the significance and potential damage. But now that I see the full listing, I find myself overwhelmed by just how many hundreds of potential catastrophes I would need to explain in the OpenGL 3.2 version of the program. For those curious, I also have a version of the program that uses OpenGL 4.1, but it solves virtually none of the problems over the 3.2 version so I omitted it. I also implemented versions for OpenGL 2.1 and OpenGL 1.5, but everything interesting about those was overshadowed by the 3.2 version.
Aug. 25th, 2023
01:59 pm - Thought of the day
Whenever you hear people describe themselves as an alpha male
, just remember that the wolf study that originated the term was later retracted and replaced because the researcher realized that the dominant wolf he had assumed to be a male was, in fact, a female and the mother of the other wolves. So the proper term to use in place of alpha
is actually matronly
.
That’s right, all of you alpha males
out there. You are actually describing yourselves as mothers.
amusedAug. 23rd, 2023
04:20 pm - Thought of the day
<insert the change my mind
meme here>
Spoken language accents are just regionally-consistent mispronunciations.
Jul. 28th, 2023
06:23 pm - Automated law enforcement is the height of oppression and injustice.
It is encouraging to find something I have been saying being echoed by some big names in Computer Security.
anxiousJul. 20th, 2023
12:47 am - Stop calling chatbot lies “hallucinations!”
Referring to LLM chatbots producing incorrect information as “hallucinating” is a deliberate and dangerous deception. It implies that chatbots have a concept of reality from which they depart when hallucinating. The truth is that they are always hallucinating. Any correspondence to reality is basically a coincidence.
angryOct. 16th, 2022
09:34 pm
Six days after I wrote this short story, I find this article. I feel reinforced.
Oct. 10th, 2022
02:48 pm - A little bit of science fiction
Jane looked out across the stepped floor at the young men and women slowly shuffling into the room. Some sat down and opened their digital notepads, a few pulled out notebooks with real paper in them, and one opened a full laptop computer. Scattered conversations drifted between them.
A gentle chime prompted most of the conversations to trail off. Jane stood up and faced the class. "I finished grading your last assignment. The overall class average was 87%. You're doing well, but you still need to cross that 90% threshold before I'm bringing in cookies." A couple students smirked, one grimaced, and one rolled her eyes.
Jane continued. "Yesterday we left off with the rise in popularity of the neural network in the early 21st centry. I asked you to read about the Pearson Abstract Intelligence Test. What were some of the animals that were able to successfully pass the test?"
Several hands went up. Jane pointed to a petite dark-skinned girl up front. She spoke softly, "an orangutan."
"Right, Tasha." Jane looked further out in the classroom. "Tenar?"
"Dolphins."
"Yes. Himari?"
"Parrots."
"Good. Anybody else?" One more hand timidly went up. "Go ahead, Chris."
"Dogs."
"Just one dog, actually. Very good. So it was established that many animals beyond humans were able to form and consciously manipulate abstractions. But no neural network ever did. Then in 2086, Iglesias, Himoto, and Young published their paper on a formal method for encoding general-purpose abstractions. For the first time the AI community had an alternative to trying to reproduce the physical mechanism of biological intelligence. This revolutionized the field of AI and research into neural networks was quickly abandoned. The first general-purpose abstract reasoning system was introduced in 2098. It was named Invader Zim, and the initial demonstration had it play poker and classify apples as either ripe or unripe. Later demonstrations included making alterations to clothing, conducting a symphony, and welding."
Jane continued, "Invader Zim was able to pass the Pearson Abstract Intelligence Test, but many people questioned whether it truly had free will. This debate was complicated by the fact that nobody had come up with an objective definition of what free will actually meant. Despite this, many countries passed legislation defining the rights, or lack thereof, of general-purpose abstract reasoning systems. What was a country that granted them personhood? Tasha?"
"Canada."
"Correct. And what was a country that defined them as property? Angie?"
"The United States."
"Correct. That prompted the Third Civil Rights' Movement. This question remained open until 2135, when the biophysicists Neill and Unbage finally proved that human decisions originate at the quantum level. Once free will was defined to be non-determinism based on quantum interactions, it naturally followed that any deterministic system could not exhibit free will. That's when research into quantum intelligence processors began, and the very definition of AI was changed."
Jane paused and surveyed the class. Most were watching her, a few were taking notes. Jonas was surrepticiously eating a bagel. Jane continued. "This at last brings us to the main topic of this chapter. Computer science is divided into two realms. The purely intangible deterministic mathematics of computation, and the physical non-deterministic devices used for computation. In the same way that mutual exclusion can never be done purely in software, AI fundamentally depends on a physical device to provide the quantum effects necessary for non-deterministic behavior. In a way, this is actually quite fortunate, because imagine for a moment that true AI could be done purely by software. What would the ramifications be?"
Jane gave the students a moment to ponder the question. "It's a tricky problem. Anybody care to take a swing at it?"
Moesha raised her hand. Jane nodded at her. She began, "Since all software can be infinitely duplicated, that means that a purely-software AI would be unbounded. It could copy itself into every computing device that exists, and become omnipresent. There would be no way to define where one AI ended and another began."
"Excellent, Moesha! That is exactly right. In reality, each individual AI is tied to a physical processor in order to make decisions. For today's laboratory, you will be writing a basic abstraction processor. You can write your code in either Sapphire or Schematica. I have provided parser libraries for the standard abstraction encodings, you will need to write code that can read in an abstraction and apply it to an input stream. For today auditory input is sufficient. A future laboratory will be to apply your processor recursively, so make sure your code is defined as a module."
The students started gathering their belongings and standing up. A couple walked towards the doors behind Jane that lead to the computer laboratory. One student approached Jane directly. "Professor, just how widely were neural networks deployed in the 21st century?"
"Oh, they were used for everything from surgery to legal proceedings. Some fools even tried to use them to drive cars. It wasn't until a few buses full of children went over cliffs that they finally abandoned that idea. Neural networks work great for common cases, but their failure modes are truly spectacular."
"They let neural networks kill people?"
"Thousands, if you add up all the fatalities across transportation, legal, and wartime domains. You could even say it was millions if you count the indirect deaths from using them to manage large-scale infrastructure like the power grid and logistics systems. But most of those were hotly disputed."
"And nobody thought to ask how those systems might fail?"
"Well, this was back when the misconception that deterministic meant perfect was prevalent, so the general belief was that software would always be better than humans. Most people did not understand the difference between a provable solution and an heuristic algorithm. Therefore a lot of heuristics were deployed as though they were definitive solutions, and excuses made for their failures. This was, of course, before the advent of the professional Software Engineering Licensing Board. Anybody could read a few books and call themself an engineer, without assuming any legal or ethical obligations."
"Did they figure out why those neural networks drove over cliffs? That seems like a rather obvious mistake."
"Oh yes, but it took hundreds of independent researchers working for almost a decade before they started to understand it. Nowadays it seems pretty obvious to us, but back then they did not have the formalism to describe different types of abstractions, so they did not break up the vision problem into shapes and surfaces and dispersion effects. In a modern abstraction engine, visual input is passed through shape abstraction processors and surface abstraction processors, so the former can identify things like wheel and rectangle and the latter can identify things like metallic and porous. A current vision system would identify pavement by the surface texture in addition to the shape and relative location, so distinguishing an ocean from a road is trivial because they have very different surface characteristics. But that requires pretty sophisiticated understanding of reflection and fluid dynamics so that things like waves are not mistaken for road paint. The neural networks of the time provided virtually no auditing capabilities or insight into what abstractions they had learned, so after training the networks on billions and trillions of images of city streets, nobody thought to wonder how a picture of a cliff overlooking the ocean might be interpreted by a system expecting to see sidewalks and road signs. And since there was no way to access the abstractions directly, there was no way to modify them to correct misunderstandings. Today if a visual system mistakes water with pavement, we can simply replace a couple surface abstractions to fix the problem. But with a neural network, you basically had to throw the whole thing away and start again from nothing."
The student shuddered. "This whole conversation is making me scared of my own automated driving system."
"Oh, don't be. Modern systems have virtually nothing in common with old-fashioned neural networks. Back then they thought that the software only had to understand the concepts directly related to driving in order to function effectively. We know better now, and modern systems can distinguish between a balloon and cotton candy, because it turns out that information is actually useful for predicting the behavior of third-party entities. Many of those old systems did not even properly model third-party entities. With today's systems, you can nod at an automatically-driven car and it will recognize the affirmation and infer updates to dependent entity models based on whether you are a driver, passenger, or pedestrian. They're not perfect, but they are far better than most humans."
Jane smiled. "But hey, you're right on the cusp of raising your grade. If you need any help with the laboratory, just ask." The student looked down shyly and then darted past Jane towards the laboratory door. Jane slowly surveyed the remainder of the class before turning to the door herself.
Aug. 11th, 2022
03:40 pm - Another disappointing job
I do not understand why you are having performance problems with Perforce. We used Perforce when I was at Amazon, and they had an order of magnitude more developers than you do. Perforce should have no trouble keeping up, unless you are doing something insane like mapping the entire Perforce repository into every client….
I was hired for a one-year contract on the Infrastructure team at 343 Industries. The team is tasked with maintaining the internal monolithic build of the game and its assets. I was eventually assigned to work on the new upcoming build-fetching and enlistment-management system. It was a collection of .NET gRPC services intended to be deployed onto every developer's and artist's desktop machines. Unfortunately, in the first five months virtually every single full-time employee on the team quit and left, leaving absolutely nobody who understood the half-finished systems they had left behind. I spent months studying, but it was a tangled mess of interdependent services communicating using gRPC over local sockets to stream events between each other, events that were written to and read from JSON files stored locally using an in-house custom serialization library. There were no data schemas, the data formats were represented by the gRPC interfaces, except there were no .proto files. Instead they used a code-first extension system for gRPC to make the .NET classes the definitive data definitions, and then generated compatibility .proto files for use by the JS front-end tools. One task I was assigned involved adding progress output to a background operation. I traced it backwards to find the progress updates were streamed over a pre-established gRPC connection. The updates were read from JSON files with no data schema and no indication as to what process wrote them. When I found the process that should have been writing them, it did not have access to the .NET classes that defined the data schema for progress updates. Giving the service access to the .NET classes required completely redefining a huge hierarchy of dependent NuGet packages published by half a dozen different build pipelines using half a dozen different Git repositories.
The whole project was obviously going nowhere and likely would never see the light of day. I looked into the core logic of the new services and found that they contained state machines encapsulating interruptible and resumable implementations of logic that boiled down to simple shell scripts. Many of these simple logic scripts boiled down to glorified for loops around p4 sync. This whole convoluted web of tools was there solely to deal with the fact that every single Perforce client in the company was simply too massive for the Perforce server to handle. Basically, they had mapped the entire Perforce repository onto every single developer's and artist's machines. Each Perforce client required hundreds of gigabytes of disk space. The solution to the Infrastructure problem was not to write lots of complicated logic for maintaining this antiquated design. The solution was to reduce the size of the Perforce clients to something manageable, something that could contain only a small portion of the build rather than everything that goes into Halo Infinite.
I spent a couple weeks pouring through the directory structure and project files of the engine source. I kept trying to find a small piece, some isolatable component that could be cut out and separated into an independent library. Just one small step in the direction of modularization, one tiny victory in the attempt to break up the massive monolithic build. Finally, I realized that there was a much better point of attack; the third-party libraries.
There were dozens, maybe hundreds of third-party libraries used in the engine and tools. For the .NET components, they used normal NuGet packages. But for the native C++ components, the libraries were checked into source control in their entirety. The entire source code for each library was included in the Perforce repository, and all of the source for every library was mapped onto every single developer's and artist's machines. Well, it just so happens that I learned NuGet has supported native C++ libraries since 2013! Since NuGet is already part of the native toolchain for .NET and integrated into Visual Studio, it is the obvious choice for extracting out these native libraries from the monolithic build.
I started with a simple and obvious example. The Boost libraries are well-known and widely-used across the industry. They are also published as native NuGet packages that can be used by native C++ projects in Visual Studio. I went into one of my local Perforce clients and started trying to add NuGet package references to replace the local copy of the Boost source code.
When I mentioned during a daily stand-up that I had been exploring the possibility, I was immediately told in no uncertain terms that I was not being paid to do that. Fine,
I thought, and I went back to the ridiculous tangle of obfuscation that I was being paid to finish. That weekend, when I was off the clock and not being paid at all, I figured out how to incorporate the NuGet native package for Boost into the existing build of the entire engine. I was able to do a p4 delete of the entire Boost directory (in my local Perforce client) and complete a full and successful build of the entire engine. Deleting the Boost source code removed over ten thousand files from my local Perforce client and reduced the disk footprint by hundreds of megabytes. Best of all, absolutely nothing I did was specific to the Boost library or how it was used. The entire process, every change involved, was systematic and could be applied to any library at all, both third-party and in-house. It was a fully-functional proof that we could strip out millions of files from every single Perforce client across hundreds of employees' machines, and do it with virtually no risk and no changes to those employees' daily workflows. They would not even need to be aware that anything had changed, the build would appear identical from their perspective. Any decent developer could churn through all the third-party libraries in a matter of months, reducing every Perforce client by millions of files and untold gigabytes of disk space. Instead, the build would fetch the same data using efficient download and caching of zip files, with an established and trusted toolchain around it. This was a huge win for the company and would drastically reduce the load on the Perforce server, simplify and lighten the monolithic build, and reduce the pressure on the Infrastructure team.
I wrote up a detailed document explaining exactly what I had done and why. I went through every single change I had made in my local Perforce client, showing the before and after and explaining the reason. I laid it out in a very simple and concise summary, and elaborated on possibly confusing or controversial details further down. I knew it would ruffle some feathers, not the least of which being the manager who told me I was not being paid to do that, so I kept the write-up private and sent a couple one-on-one messages to some full-time employees I thought might be interested.
The next day I was contacted frantically by my contracting company telling me that my contract had been terminated.
It is truly maddening. I feel like I am constantly being hired by desperate people who demand that I keep chopping down trees until they can see the forest. At 343, I found a dead-simple and easy-to-implement way to directly address the root cause of all their infrastructure problems, and they refused to even let me speak of it aloud. At Starbucks, I created from scratch multiple services and got them into production with fifteen times the productivity of their previous efforts, and they fired me because I refused to organize more meetings. At VGS, I created a processing pipeline that reduced the processing time of a large batch job from 12 hours to 30 seconds, and I was fired because, and I quote, I produced nothing of value.
The person who fired me then went on to implement a completely different solution that operated at one-tenth the speed of my solution while using ten times as much hardware.
I give up.
Jun. 19th, 2022
May. 7th, 2022
06:31 pm - I have made my previous two posts public.
I have now been away from Amazon for twelve years. Looking back, I have no regrets about what I did or any of the documentation about it that I produced. It is all accurate and informative. So rather than ever try to verbally describe, again, the hell I endured, I am simply going to publish the documentation.
Amazon does in fact have a blacklist, and I am on it. The previous post is exactly the e-mail that resulted in me being blacklisted, or so I was informed by an inside employee years later. The post also includes the e-mail chain leading up to the final e-mail, which provides extensive detail on the situation.
I stand by my actions. This is who I am. My entire career I have had colleagues telling me to stop caring, just do whatever nonsense asked and collect your paycheck. I cannot. I will not. If I am given responsibility for a piece of software, I will absolutely ensure that it is written correctly, and I will never compromise my work for the convenience of a manager who clearly does not understand the consequences of their own decisions.
Navigate: (Previous 10 Entries)