Michael Gold wrote:
|
| In a context-independent implementation, we'll have to synthesize a wrapper
| function in device independent code...
I think that's true.
| ... and devise a mechanism whereby the
| driver informs the higher level code every time it wishes to change the
| dispatch table, and/or update it directly through an additional level of
| indirection after registering the entry point with the device-independent
| code. ...
I'm not sure either of those is true.
I think it's particularly important to have the driver own the
dispatch table. Validating the table after a state change needs to be
efficient, and forcing the driver to request higher level code to
perform the update seems unnecessarily slow to me. I suspect we agree
on this. :-)
Unless I've missed something, an additional level of indirection is
also unnecessary, provided that all drivers use the same dispatch
table entry for a given function. The registration mechanism that we
use for extension enumerants is a good example of a way to guarantee
dispatch table consistency.
My original proposal didn't solve the problem that John Zulauf
mentioned, that is, GetProcAddress needs to return an appropriate
pointer even when the current context doesn't support the extension in
question. Solving this in the most app-friendly way does take a
little more effort. One approach could be as follows.
Recall that each dynamic dispatch routine looks something
like this:
offsetNNN: word 0
dispNNN: load r1,dispatchTableBase
add r1,offsetNNN
jump (r1)
We don't know what the correct value for the offset will be
until we make a call to a driver that implements the extension
function. So we have to force either an explicit test, or a
jump to a thunk that performs the test and can later patch up
the jump. The latter is faster after the resolution has been
performed, but implementation is more likely to be
machine-dependent, so I'll just show the straightforward test
code here.
We need one more thing: Each driver must implement a
GetProcOffsetEXT that returns the dispatch-table offset for
a given function name, if it implements that function.
nameNNN: byte "filled in by GetProcAddress"
offsetNNN: word 0
dispNNN: load r1,offsetNNN
jzero r1,queryNNN
jumpNNN: add r1,dispatchTableBase
jump (r1)
queryNNN: load rParam,&nameNNN
load r1,dispatchTableBase
add r1,offsetOfGetProcOffsetEXT
jsr (r1)
store rResult,offsetNNN
jnzero rResult,dispNNN
ret
Hopefully that pseudo-assembly code is clear. Basically we're
just testing for a null pointer, querying the driver for the
given function name if the pointer is null, and saving the
offset returned in a result register by the driver. If the
driver returns a nonzero result, then we know we can use it
for all subsequent dispatches of that function, no matter what
the context, because offsets are consistent across all
implementations.
Obviously this can be tuned, and as I mentioned, I think you
can use a thunk-based implementation if you prefer. The main
thing I haven't addressed is how to ensure that calling a
function when the current context doesn't support it is
handled politely. Personally I don't think that's too
important, but if anyone wants to propose a solution, please
feel free.
| Whether or not the overhead and complexity of the latter case is
| manageable, my primary objection is that it simply doesn't solve a problem
| worth solving, since the application must already manage per-context state
| (e.g. is the extension supported?) and the function pointers are one more
| piece of state to be stored in some per-context application-level data
| structure. ...
Application-level data structures aren't accessible to black-box
libraries.
But let's talk about design principles for a moment. I think the
principle underlying a desire for context-dependent pointers is the
notion that interface and implementation are separated at the binary
level (as opposed to the source level), and because of the limitations
of current programming languages, this separation must be exposed all
the way up to code at the application level.
This is a defensible point of view, but to iron out *all* the problems
that arise from radical separation of binary interface and
implementation, you need industrial-strength mechanisms like COM.
That's what they're designed for.
I would argue that one of the reasons people prefer OpenGL to D3D is
the simplicity of the interface model. I'd like to maintain that
simplicity if I can. I suspect that one of the costs of doing so is
the requirement for implementations to support context-independent
function pointers. Otherwise you are, in effect, taking a step down
the path of requiring an explicit query for each driver interface, and
then accessing all functions indirectly through a driver-specific
interface structure in the app.
It also just occurred to me that by forcing the indirection to happen
at the application level, you not only add the indirection you were
trying to avoid by using context-dependent pointers, you also move the
indirection out of the library so that the library implementor can't
employ a more-efficient mechanism if he knows of one.
Allen