Compare commits

...

3 Commits

Author SHA1 Message Date
728d9aeddc
add lib-not-dr as requirement 2023-10-16 22:16:51 +08:00
79cdba7b9c
Enhance | logger with Template 2023-10-16 22:13:21 +08:00
1cfd6cc066
sync pyglet 2023-10-16 22:13:04 +08:00
13 changed files with 423 additions and 443 deletions

View File

@ -266,30 +266,29 @@ class DWRITE_CLUSTER_METRICS(ctypes.Structure):
class IDWriteFontFileStream(com.IUnknown): class IDWriteFontFileStream(com.IUnknown):
_methods_ = [ _methods_ = [
('ReadFileFragment', ('ReadFileFragment',
com.STDMETHOD(c_void_p, POINTER(c_void_p), UINT64, UINT64, POINTER(c_void_p))), com.STDMETHOD(POINTER(c_void_p), UINT64, UINT64, POINTER(c_void_p))),
('ReleaseFileFragment', ('ReleaseFileFragment',
com.STDMETHOD(c_void_p, c_void_p)), com.STDMETHOD(c_void_p)),
('GetFileSize', ('GetFileSize',
com.STDMETHOD(c_void_p, POINTER(UINT64))), com.STDMETHOD(POINTER(UINT64))),
('GetLastWriteTime', ('GetLastWriteTime',
com.STDMETHOD(c_void_p, POINTER(UINT64))), com.STDMETHOD(POINTER(UINT64))),
] ]
class IDWriteFontFileLoader_LI(com.IUnknown): # Local implementation use only. class IDWriteFontFileLoader_LI(com.IUnknown): # Local implementation use only.
_methods_ = [ _methods_ = [
('CreateStreamFromKey', ('CreateStreamFromKey',
com.STDMETHOD(c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream)))) com.STDMETHOD(c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream))))
] ]
class IDWriteFontFileLoader(com.pIUnknown): class IDWriteFontFileLoader(com.pIUnknown):
_methods_ = [ _methods_ = [
('CreateStreamFromKey', ('CreateStreamFromKey',
com.STDMETHOD(c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream)))) com.STDMETHOD(c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream))))
] ]
class IDWriteLocalFontFileLoader(IDWriteFontFileLoader, com.pIUnknown): class IDWriteLocalFontFileLoader(IDWriteFontFileLoader, com.pIUnknown):
_methods_ = [ _methods_ = [
('GetFilePathLengthFromKey', ('GetFilePathLengthFromKey',
@ -452,13 +451,13 @@ DWRITE_READING_DIRECTION_LEFT_TO_RIGHT = 0
class IDWriteTextAnalysisSource(com.IUnknown): class IDWriteTextAnalysisSource(com.IUnknown):
_methods_ = [ _methods_ = [
('GetTextAtPosition', ('GetTextAtPosition',
com.METHOD(HRESULT, c_void_p, UINT32, POINTER(c_wchar_p), POINTER(UINT32))), com.STDMETHOD(UINT32, POINTER(c_wchar_p), POINTER(UINT32))),
('GetTextBeforePosition', ('GetTextBeforePosition',
com.STDMETHOD(UINT32, c_wchar_p, POINTER(UINT32))), com.STDMETHOD(UINT32, POINTER(c_wchar_p), POINTER(UINT32))),
('GetParagraphReadingDirection', ('GetParagraphReadingDirection',
com.METHOD(DWRITE_READING_DIRECTION)), com.METHOD(DWRITE_READING_DIRECTION)),
('GetLocaleName', ('GetLocaleName',
com.STDMETHOD(c_void_p, UINT32, POINTER(UINT32), POINTER(c_wchar_p))), com.STDMETHOD(UINT32, POINTER(UINT32), POINTER(c_wchar_p))),
('GetNumberSubstitution', ('GetNumberSubstitution',
com.STDMETHOD(UINT32, POINTER(UINT32), c_void_p)), com.STDMETHOD(UINT32, POINTER(UINT32), c_void_p)),
] ]
@ -467,7 +466,7 @@ class IDWriteTextAnalysisSource(com.IUnknown):
class IDWriteTextAnalysisSink(com.IUnknown): class IDWriteTextAnalysisSink(com.IUnknown):
_methods_ = [ _methods_ = [
('SetScriptAnalysis', ('SetScriptAnalysis',
com.STDMETHOD(c_void_p, UINT32, UINT32, POINTER(DWRITE_SCRIPT_ANALYSIS))), com.STDMETHOD(UINT32, UINT32, POINTER(DWRITE_SCRIPT_ANALYSIS))),
('SetLineBreakpoints', ('SetLineBreakpoints',
com.STDMETHOD(UINT32, UINT32, c_void_p)), com.STDMETHOD(UINT32, UINT32, c_void_p)),
('SetBidiLevel', ('SetBidiLevel',
@ -524,7 +523,7 @@ class TextAnalysis(com.COMObject):
analyzer.AnalyzeScript(self, 0, text_length, self) analyzer.AnalyzeScript(self, 0, text_length, self)
def SetScriptAnalysis(self, this, textPosition, textLength, scriptAnalysis): def SetScriptAnalysis(self, textPosition, textLength, scriptAnalysis):
# textPosition - The index of the first character in the string that the result applies to # textPosition - The index of the first character in the string that the result applies to
# textLength - How many characters of the string from the index that the result applies to # textLength - How many characters of the string from the index that the result applies to
# scriptAnalysis - The analysis information for all glyphs starting at position for length. # scriptAnalysis - The analysis information for all glyphs starting at position for length.
@ -542,10 +541,10 @@ class TextAnalysis(com.COMObject):
return 0 return 0
# return 0x80004001 # return 0x80004001
def GetTextBeforePosition(self, this, textPosition, textString, textLength): def GetTextBeforePosition(self, textPosition, textString, textLength):
raise Exception("Currently not implemented.") raise Exception("Currently not implemented.")
def GetTextAtPosition(self, this, textPosition, textString, textLength): def GetTextAtPosition(self, textPosition, textString, textLength):
# This method will retrieve a substring of the text in this layout # This method will retrieve a substring of the text in this layout
# to be used in an analysis step. # to be used in an analysis step.
# Arguments: # Arguments:
@ -568,7 +567,7 @@ class TextAnalysis(com.COMObject):
def GetParagraphReadingDirection(self): def GetParagraphReadingDirection(self):
return 0 return 0
def GetLocaleName(self, this, textPosition, textLength, localeName): def GetLocaleName(self, textPosition, textLength, localeName):
self.__local_name = c_wchar_p("") # TODO: Add more locales. self.__local_name = c_wchar_p("") # TODO: Add more locales.
localeName[0] = self.__local_name localeName[0] = self.__local_name
textLength[0] = self._textlength - textPosition textLength[0] = self._textlength - textPosition
@ -954,16 +953,16 @@ class IDWriteTextLayout1(IDWriteTextLayout, IDWriteTextFormat, com.pIUnknown):
class IDWriteFontFileEnumerator(com.IUnknown): class IDWriteFontFileEnumerator(com.IUnknown):
_methods_ = [ _methods_ = [
('MoveNext', ('MoveNext',
com.STDMETHOD(c_void_p, POINTER(BOOL))), com.STDMETHOD(POINTER(BOOL))),
('GetCurrentFontFile', ('GetCurrentFontFile',
com.STDMETHOD(c_void_p, c_void_p)), com.STDMETHOD(c_void_p)),
] ]
class IDWriteFontCollectionLoader(com.IUnknown): class IDWriteFontCollectionLoader(com.IUnknown):
_methods_ = [ _methods_ = [
('CreateEnumeratorFromKey', ('CreateEnumeratorFromKey',
com.STDMETHOD(c_void_p, c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileEnumerator)))), com.STDMETHOD(c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileEnumerator)))),
] ]
@ -971,20 +970,12 @@ class MyFontFileStream(com.COMObject):
_interfaces_ = [IDWriteFontFileStream] _interfaces_ = [IDWriteFontFileStream]
def __init__(self, data): def __init__(self, data):
super().__init__()
self._data = data self._data = data
self._size = len(data) self._size = len(data)
self._ptrs = [] self._ptrs = []
def AddRef(self, this): def ReadFileFragment(self, fragmentStart, fileOffset, fragmentSize, fragmentContext):
return 1
def Release(self, this):
return 1
def QueryInterface(self, this, refiid, tester):
return 0
def ReadFileFragment(self, this, fragmentStart, fileOffset, fragmentSize, fragmentContext):
if fileOffset + fragmentSize > self._size: if fileOffset + fragmentSize > self._size:
return 0x80004005 # E_FAIL return 0x80004005 # E_FAIL
@ -997,14 +988,14 @@ class MyFontFileStream(com.COMObject):
fragmentContext[0] = None fragmentContext[0] = None
return 0 return 0
def ReleaseFileFragment(self, this, fragmentContext): def ReleaseFileFragment(self, fragmentContext):
return 0 return 0
def GetFileSize(self, this, fileSize): def GetFileSize(self, fileSize):
fileSize[0] = self._size fileSize[0] = self._size
return 0 return 0
def GetLastWriteTime(self, this, lastWriteTime): def GetLastWriteTime(self, lastWriteTime):
return 0x80004001 # E_NOTIMPL return 0x80004001 # E_NOTIMPL
@ -1012,21 +1003,13 @@ class LegacyFontFileLoader(com.COMObject):
_interfaces_ = [IDWriteFontFileLoader_LI] _interfaces_ = [IDWriteFontFileLoader_LI]
def __init__(self): def __init__(self):
super().__init__()
self._streams = {} self._streams = {}
def QueryInterface(self, this, refiid, tester): def CreateStreamFromKey(self, fontfileReferenceKey, fontFileReferenceKeySize, fontFileStream):
return 0
def AddRef(self, this):
return 1
def Release(self, this):
return 1
def CreateStreamFromKey(self, this, fontfileReferenceKey, fontFileReferenceKeySize, fontFileStream):
convert_index = cast(fontfileReferenceKey, POINTER(c_uint32)) convert_index = cast(fontfileReferenceKey, POINTER(c_uint32))
self._ptr = ctypes.cast(self._streams[convert_index.contents.value]._pointers[IDWriteFontFileStream], self._ptr = ctypes.cast(self._streams[convert_index.contents.value].as_interface(IDWriteFontFileStream),
POINTER(IDWriteFontFileStream)) POINTER(IDWriteFontFileStream))
fontFileStream[0] = self._ptr fontFileStream[0] = self._ptr
return 0 return 0
@ -1039,6 +1022,7 @@ class MyEnumerator(com.COMObject):
_interfaces_ = [IDWriteFontFileEnumerator] _interfaces_ = [IDWriteFontFileEnumerator]
def __init__(self, factory, loader): def __init__(self, factory, loader):
super().__init__()
self.factory = cast(factory, IDWriteFactory) self.factory = cast(factory, IDWriteFactory)
self.key = "pyglet_dwrite" self.key = "pyglet_dwrite"
self.size = len(self.key) self.size = len(self.key)
@ -1057,7 +1041,7 @@ class MyEnumerator(com.COMObject):
def AddFontData(self, fonts): def AddFontData(self, fonts):
self._font_data = fonts self._font_data = fonts
def MoveNext(self, this, hasCurrentFile): def MoveNext(self, hasCurrentFile):
self.current_index += 1 self.current_index += 1
if self.current_index != len(self._font_data): if self.current_index != len(self._font_data):
@ -1087,7 +1071,7 @@ class MyEnumerator(com.COMObject):
pass pass
def GetCurrentFontFile(self, this, fontFile): def GetCurrentFontFile(self, fontFile):
fontFile = cast(fontFile, POINTER(IDWriteFontFile)) fontFile = cast(fontFile, POINTER(IDWriteFontFile))
fontFile[0] = self._font_files[self.current_index] fontFile[0] = self._font_files[self.current_index]
return 0 return 0
@ -1097,24 +1081,14 @@ class LegacyCollectionLoader(com.COMObject):
_interfaces_ = [IDWriteFontCollectionLoader] _interfaces_ = [IDWriteFontCollectionLoader]
def __init__(self, factory, loader): def __init__(self, factory, loader):
super().__init__()
self._enumerator = MyEnumerator(factory, loader) self._enumerator = MyEnumerator(factory, loader)
def AddFontData(self, fonts): def AddFontData(self, fonts):
self._enumerator.AddFontData(fonts) self._enumerator.AddFontData(fonts)
def AddRef(self, this): def CreateEnumeratorFromKey(self, factory, key, key_size, enumerator):
self._i = 1 self._ptr = ctypes.cast(self._enumerator.as_interface(IDWriteFontFileEnumerator),
return 1
def Release(self, this):
self._i = 0
return 1
def QueryInterface(self, this, refiid, tester):
return 0
def CreateEnumeratorFromKey(self, this, factory, key, key_size, enumerator):
self._ptr = ctypes.cast(self._enumerator._pointers[IDWriteFontFileEnumerator],
POINTER(IDWriteFontFileEnumerator)) POINTER(IDWriteFontFileEnumerator))
enumerator[0] = self._ptr enumerator[0] = self._ptr
@ -2418,7 +2392,7 @@ class Win32DirectWriteFont(base.Font):
# Note: RegisterFontLoader takes a pointer. However, for legacy we implement our own callback interface. # Note: RegisterFontLoader takes a pointer. However, for legacy we implement our own callback interface.
# Therefore we need to pass to the actual pointer directly. # Therefore we need to pass to the actual pointer directly.
cls._write_factory.RegisterFontFileLoader(cls._font_loader.pointers[IDWriteFontFileLoader_LI]) cls._write_factory.RegisterFontFileLoader(cls._font_loader.as_interface(IDWriteFontFileLoader_LI))
cls._font_collection_loader = LegacyCollectionLoader(cls._write_factory, cls._font_loader) cls._font_collection_loader = LegacyCollectionLoader(cls._write_factory, cls._font_loader)
cls._write_factory.RegisterFontCollectionLoader(cls._font_collection_loader) cls._write_factory.RegisterFontCollectionLoader(cls._font_collection_loader)
@ -2472,7 +2446,7 @@ class Win32DirectWriteFont(base.Font):
cls._font_collection_loader = LegacyCollectionLoader(cls._write_factory, cls._font_loader) cls._font_collection_loader = LegacyCollectionLoader(cls._write_factory, cls._font_loader)
cls._write_factory.RegisterFontCollectionLoader(cls._font_collection_loader) cls._write_factory.RegisterFontCollectionLoader(cls._font_collection_loader)
cls._write_factory.RegisterFontFileLoader(cls._font_loader.pointers[IDWriteFontFileLoader_LI]) cls._write_factory.RegisterFontFileLoader(cls._font_loader.as_interface(IDWriteFontFileLoader_LI))
cls._font_collection_loader.AddFontData(cls._font_cache) cls._font_collection_loader.AddFontData(cls._font_cache)

View File

@ -170,14 +170,14 @@ class Attribute:
self.name = name self.name = name
self.location = location self.location = location
self.count = count self.count = count
self.gl_type = gl_type self.gl_type = gl_type
self.c_type = _c_types[gl_type]
self.normalize = normalize self.normalize = normalize
self.align = sizeof(self.c_type) self.c_type = _c_types[gl_type]
self.size = count * self.align
self.stride = self.size self.element_size = sizeof(self.c_type)
self.byte_size = count * self.element_size
self.stride = self.byte_size
def enable(self): def enable(self):
"""Enable the attribute.""" """Enable the attribute."""
@ -216,15 +216,11 @@ class Attribute:
:rtype: `AbstractBufferRegion` :rtype: `AbstractBufferRegion`
""" """
byte_start = self.stride * start return buffer.get_region(start, count)
byte_size = self.stride * count
array_count = self.count * count
ptr_type = POINTER(self.c_type * array_count)
return buffer.get_region(byte_start, byte_size, ptr_type)
def set_region(self, buffer, start, count, data): def set_region(self, buffer, start, count, data):
"""Set the data over a region of the buffer.
"""Set the data over a region of the buffer.
:Parameters: :Parameters:
`buffer` : AbstractMappable` `buffer` : AbstractMappable`
The buffer to modify. The buffer to modify.
@ -234,9 +230,10 @@ class Attribute:
Number of vertices to set. Number of vertices to set.
`data` : A sequence of data components. `data` : A sequence of data components.
""" """
byte_start = self.stride * start byte_start = self.stride * start # byte offset
byte_size = self.stride * count byte_size = self.stride * count # number of bytes
array_count = self.count * count array_count = self.count * count # umber of values
data = (self.c_type * array_count)(*data) data = (self.c_type * array_count)(*data)
buffer.set_data_region(data, byte_start, byte_size) buffer.set_data_region(data, byte_start, byte_size)

View File

@ -11,6 +11,8 @@ the buffer.
import sys import sys
import ctypes import ctypes
from functools import lru_cache
import pyglet import pyglet
from pyglet.gl import * from pyglet.gl import *
@ -98,36 +100,6 @@ class AbstractBuffer:
raise NotImplementedError('abstract') raise NotImplementedError('abstract')
class AbstractMappable:
def get_region(self, start, size, ptr_type):
"""Map a region of the buffer into a ctypes array of the desired
type. This region does not need to be unmapped, but will become
invalid if the buffer is resized.
Note that although a pointer type is required, an array is mapped.
For example::
get_region(0, ctypes.sizeof(c_int) * 20, ctypes.POINTER(c_int * 20))
will map bytes 0 to 80 of the buffer to an array of 20 ints.
Changes to the array may not be recognised until the region's
:py:meth:`AbstractBufferRegion.invalidate` method is called.
:Parameters:
`start` : int
Offset into the buffer to map from, in bytes
`size` : int
Size of the buffer region to map, in bytes
`ptr_type` : ctypes pointer type
Pointer type describing the array format to create
:rtype: :py:class:`AbstractBufferRegion`
"""
raise NotImplementedError('abstract')
class BufferObject(AbstractBuffer): class BufferObject(AbstractBuffer):
"""Lightweight representation of an OpenGL Buffer Object. """Lightweight representation of an OpenGL Buffer Object.
@ -222,27 +194,28 @@ class BufferObject(AbstractBuffer):
return f"{self.__class__.__name__}(id={self.id}, size={self.size})" return f"{self.__class__.__name__}(id={self.id}, size={self.size})"
class MappableBufferObject(BufferObject, AbstractMappable): class AttributeBufferObject(BufferObject):
"""A buffer with system-memory backed store. """A buffer with system-memory backed store.
Updates to the data via `set_data`, `set_data_region` and `map` will be Updates to the data via `set_data` and `set_data_region` will be held
held in local memory until `bind` is called. The advantage is that fewer in local memory until `buffer_data` is called. The advantage is that
OpenGL calls are needed, increasing performance. fewer OpenGL calls are needed, which can increasing performance at the
expense of system memory.
There may also be less performance penalty for resizing this buffer.
Updates to data via :py:meth:`map` are committed immediately.
""" """
def __init__(self, size, usage=GL_DYNAMIC_DRAW): def __init__(self, size, attribute, usage=GL_DYNAMIC_DRAW):
super(MappableBufferObject, self).__init__(size, usage) super().__init__(size, usage)
self._size = size
self.data = (ctypes.c_byte * size)() self.data = (ctypes.c_byte * size)()
self.data_ptr = ctypes.addressof(self.data) self.data_ptr = ctypes.addressof(self.data)
self._dirty_min = sys.maxsize self._dirty_min = sys.maxsize
self._dirty_max = 0 self._dirty_max = 0
def bind(self): self.attribute_stride = attribute.stride
# Commit pending data self.attribute_count = attribute.count
super(MappableBufferObject, self).bind() self.attribute_ctype = attribute.c_type
def bind(self, target=GL_ARRAY_BUFFER):
super().bind(target)
size = self._dirty_max - self._dirty_min size = self._dirty_max - self._dirty_min
if size > 0: if size > 0:
if size == self.size: if size == self.size:
@ -253,7 +226,7 @@ class MappableBufferObject(BufferObject, AbstractMappable):
self._dirty_max = 0 self._dirty_max = 0
def set_data(self, data): def set_data(self, data):
super(MappableBufferObject, self).set_data(data) super().set_data(data)
ctypes.memmove(self.data, data, self.size) ctypes.memmove(self.data, data, self.size)
self._dirty_min = 0 self._dirty_min = 0
self._dirty_max = self.size self._dirty_max = self.size
@ -263,17 +236,16 @@ class MappableBufferObject(BufferObject, AbstractMappable):
self._dirty_min = min(start, self._dirty_min) self._dirty_min = min(start, self._dirty_min)
self._dirty_max = max(start + length, self._dirty_max) self._dirty_max = max(start + length, self._dirty_max)
def map(self, invalidate=False): @lru_cache(maxsize=None)
self._dirty_min = 0 def get_region(self, start, count):
self._dirty_max = self.size
return self.data
def unmap(self): byte_start = self.attribute_stride * start # byte offset
pass byte_size = self.attribute_stride * count # number of bytes
array_count = self.attribute_count * count # number of values
def get_region(self, start, size, ptr_type): ptr_type = ctypes.POINTER(self.attribute_ctype * array_count)
array = ctypes.cast(self.data_ptr + start, ptr_type).contents array = ctypes.cast(self.data_ptr + byte_start, ptr_type).contents
return BufferObjectRegion(self, start, start + size, array) return BufferObjectRegion(self, byte_start, byte_start + byte_size, array)
def resize(self, size): def resize(self, size):
data = (ctypes.c_byte * size)() data = (ctypes.c_byte * size)()
@ -289,6 +261,8 @@ class MappableBufferObject(BufferObject, AbstractMappable):
self._dirty_min = sys.maxsize self._dirty_min = sys.maxsize
self._dirty_max = 0 self._dirty_max = 0
self.get_region.cache_clear()
class BufferObjectRegion: class BufferObjectRegion:
"""A mapped region of a MappableBufferObject.""" """A mapped region of a MappableBufferObject."""

View File

@ -23,11 +23,9 @@ primitives of the same OpenGL primitive mode.
import ctypes import ctypes
import pyglet
from pyglet.gl import * from pyglet.gl import *
from pyglet.graphics import allocation, shader, vertexarray from pyglet.graphics import allocation, shader, vertexarray
from pyglet.graphics.vertexbuffer import BufferObject, MappableBufferObject from pyglet.graphics.vertexbuffer import BufferObject, AttributeBufferObject
def _nearest_pow2(v): def _nearest_pow2(v):
@ -66,6 +64,22 @@ _gl_types = {
} }
def _make_attribute_property(attribute):
attrname = attribute.name
def _attribute_getter(self):
region = attribute.get_region(attribute.buffer, self.start, self.count)
region.invalidate()
return region.array
def _attribute_setter(self, values):
getattr(self, attrname)[:] = values
return property(_attribute_getter, _attribute_setter)
class VertexDomain: class VertexDomain:
"""Management of a set of vertex lists. """Management of a set of vertex lists.
@ -80,8 +94,10 @@ class VertexDomain:
self.attribute_meta = attribute_meta self.attribute_meta = attribute_meta
self.allocator = allocation.Allocator(self._initial_count) self.allocator = allocation.Allocator(self._initial_count)
self.attributes = [] self.attribute_names = {} # name: attribute
self.buffer_attributes = [] # list of (buffer, attributes) self.buffer_attributes = [] # list of (buffer, attributes)
self._property_dict = {} # name: property(_getter, _setter)
for name, meta in attribute_meta.items(): for name, meta in attribute_meta.items():
assert meta['format'][0] in _gl_types, f"'{meta['format']}' is not a valid atrribute format for '{name}'." assert meta['format'][0] in _gl_types, f"'{meta['format']}' is not a valid atrribute format for '{name}'."
@ -90,14 +106,19 @@ class VertexDomain:
gl_type = _gl_types[meta['format'][0]] gl_type = _gl_types[meta['format'][0]]
normalize = 'n' in meta['format'] normalize = 'n' in meta['format']
attribute = shader.Attribute(name, location, count, gl_type, normalize) attribute = shader.Attribute(name, location, count, gl_type, normalize)
self.attributes.append(attribute) self.attribute_names[attribute.name] = attribute
# Create buffer: # Create buffer:
attribute.buffer = MappableBufferObject(attribute.stride * self.allocator.capacity) attribute.buffer = AttributeBufferObject(attribute.stride * self.allocator.capacity, attribute)
attribute.buffer.element_size = attribute.stride
attribute.buffer.attributes = (attribute,)
self.buffer_attributes.append((attribute.buffer, (attribute,))) self.buffer_attributes.append((attribute.buffer, (attribute,)))
# Create custom property to be used in the VertexList:
self._property_dict[attribute.name] = _make_attribute_property(attribute)
# Make a custom VertexList class w/ properties for each attribute in the ShaderProgram:
self._vertexlist_class = type("VertexList", (VertexList,), self._property_dict)
self.vao = vertexarray.VertexArray() self.vao = vertexarray.VertexArray()
self.vao.bind() self.vao.bind()
for buffer, attributes in self.buffer_attributes: for buffer, attributes in self.buffer_attributes:
@ -107,20 +128,6 @@ class VertexDomain:
attribute.set_pointer(buffer.ptr) attribute.set_pointer(buffer.ptr)
self.vao.unbind() self.vao.unbind()
# Create named attributes for each attribute
self.attribute_names = {}
for attribute in self.attributes:
self.attribute_names[attribute.name] = attribute
def __del__(self):
# Break circular refs that Python GC seems to miss even when forced
# collection.
for attribute in self.attributes:
try:
del attribute.buffer
except AttributeError:
pass
def safe_alloc(self, count): def safe_alloc(self, count):
"""Allocate vertices, resizing the buffers if necessary.""" """Allocate vertices, resizing the buffers if necessary."""
try: try:
@ -129,7 +136,7 @@ class VertexDomain:
capacity = _nearest_pow2(e.requested_capacity) capacity = _nearest_pow2(e.requested_capacity)
self.version += 1 self.version += 1
for buffer, _ in self.buffer_attributes: for buffer, _ in self.buffer_attributes:
buffer.resize(capacity * buffer.element_size) buffer.resize(capacity * buffer.attribute_stride)
self.allocator.set_capacity(capacity) self.allocator.set_capacity(capacity)
return self.allocator.alloc(count) return self.allocator.alloc(count)
@ -141,7 +148,7 @@ class VertexDomain:
capacity = _nearest_pow2(e.requested_capacity) capacity = _nearest_pow2(e.requested_capacity)
self.version += 1 self.version += 1
for buffer, _ in self.buffer_attributes: for buffer, _ in self.buffer_attributes:
buffer.resize(capacity * buffer.element_size) buffer.resize(capacity * buffer.attribute_stride)
self.allocator.set_capacity(capacity) self.allocator.set_capacity(capacity)
return self.allocator.realloc(start, count, new_count) return self.allocator.realloc(start, count, new_count)
@ -157,7 +164,7 @@ class VertexDomain:
:rtype: :py:class:`VertexList` :rtype: :py:class:`VertexList`
""" """
start = self.safe_alloc(count) start = self.safe_alloc(count)
return VertexList(self, start, count) return self._vertexlist_class(self, start, count)
def draw(self, mode): def draw(self, mode):
"""Draw all vertices in the domain. """Draw all vertices in the domain.
@ -221,8 +228,6 @@ class VertexList:
self.domain = domain self.domain = domain
self.start = start self.start = start
self.count = count self.count = count
self._caches = {}
self._cache_versions = {}
def draw(self, mode): def draw(self, mode):
"""Draw this vertex list in the given OpenGL mode. """Draw this vertex list in the given OpenGL mode.
@ -247,7 +252,7 @@ class VertexList:
new_start = self.domain.safe_realloc(self.start, self.count, count) new_start = self.domain.safe_realloc(self.start, self.count, count)
if new_start != self.start: if new_start != self.start:
# Copy contents to new location # Copy contents to new location
for attribute in self.domain.attributes: for attribute in self.domain.attribute_names.values():
old = attribute.get_region(attribute.buffer, self.start, self.count) old = attribute.get_region(attribute.buffer, self.start, self.count)
new = attribute.get_region(attribute.buffer, new_start, self.count) new = attribute.get_region(attribute.buffer, new_start, self.count)
new.array[:] = old.array[:] new.array[:] = old.array[:]
@ -255,9 +260,6 @@ class VertexList:
self.start = new_start self.start = new_start
self.count = count self.count = count
for version in self._cache_versions:
self._cache_versions[version] = None
def delete(self): def delete(self):
"""Delete this group.""" """Delete this group."""
self.domain.allocator.dealloc(self.start, self.count) self.domain.allocator.dealloc(self.start, self.count)
@ -287,33 +289,10 @@ class VertexList:
self.domain = domain self.domain = domain
self.start = new_start self.start = new_start
for version in self._cache_versions:
self._cache_versions[version] = None
def set_attribute_data(self, name, data): def set_attribute_data(self, name, data):
attribute = self.domain.attribute_names[name] attribute = self.domain.attribute_names[name]
attribute.set_region(attribute.buffer, self.start, self.count, data) attribute.set_region(attribute.buffer, self.start, self.count, data)
def __getattr__(self, name):
"""dynamic access to vertex attributes, for backwards compatibility.
"""
domain = self.domain
if self._cache_versions.get(name, None) != domain.version:
attribute = domain.attribute_names[name]
self._caches[name] = attribute.get_region(attribute.buffer, self.start, self.count)
self._cache_versions[name] = domain.version
region = self._caches[name]
region.invalidate()
return region.array
def __setattr__(self, name, value):
# Allow setting vertex attributes directly without overwriting them:
if 'domain' in self.__dict__ and name in self.__dict__['domain'].attribute_names:
getattr(self, name)[:] = value
return
super().__setattr__(name, value)
class IndexedVertexDomain(VertexDomain): class IndexedVertexDomain(VertexDomain):
"""Management of a set of indexed vertex lists. """Management of a set of indexed vertex lists.
@ -337,6 +316,9 @@ class IndexedVertexDomain(VertexDomain):
self.index_buffer.bind_to_index_buffer() self.index_buffer.bind_to_index_buffer()
self.vao.unbind() self.vao.unbind()
# Make a custom VertexList class w/ properties for each attribute in the ShaderProgram:
self._vertexlist_class = type("IndexedVertexList", (IndexedVertexList,), self._property_dict)
def safe_index_alloc(self, count): def safe_index_alloc(self, count):
"""Allocate indices, resizing the buffers if necessary.""" """Allocate indices, resizing the buffers if necessary."""
try: try:
@ -371,7 +353,7 @@ class IndexedVertexDomain(VertexDomain):
""" """
start = self.safe_alloc(count) start = self.safe_alloc(count)
index_start = self.safe_index_alloc(index_count) index_start = self.safe_index_alloc(index_count)
return IndexedVertexList(self, start, count, index_start, index_count) return self._vertexlist_class(self, start, count, index_start, index_count)
def get_index_region(self, start, count): def get_index_region(self, start, count):
"""Get a data from a region of the index buffer. """Get a data from a region of the index buffer.

View File

@ -18,7 +18,7 @@ Interfaces can define methods::
... ...
] ]
Only use STDMETHOD or METHOD for the method types (not ordinary ctypes Only use METHOD, STDMETHOD or VOIDMETHOD for the method types (not ordinary ctypes
function types). The 'this' pointer is bound automatically... e.g., call:: function types). The 'this' pointer is bound automatically... e.g., call::
device = IDirectSound8() device = IDirectSound8()
@ -50,7 +50,7 @@ class GUID(ctypes.Structure):
('Data1', ctypes.c_ulong), ('Data1', ctypes.c_ulong),
('Data2', ctypes.c_ushort), ('Data2', ctypes.c_ushort),
('Data3', ctypes.c_ushort), ('Data3', ctypes.c_ushort),
('Data4', ctypes.c_ubyte * 8) ('Data4', ctypes.c_ubyte * 8),
] ]
def __init__(self, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8): def __init__(self, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8):
@ -64,11 +64,6 @@ class GUID(ctypes.Structure):
return 'GUID(%x, %x, %x, %x, %x, %x, %x, %x, %x, %x, %x)' % ( return 'GUID(%x, %x, %x, %x, %x, %x, %x, %x, %x, %x, %x)' % (
self.Data1, self.Data2, self.Data3, b1, b2, b3, b4, b5, b6, b7, b8) self.Data1, self.Data2, self.Data3, b1, b2, b3, b4, b5, b6, b7, b8)
def __cmp__(self, other):
if isinstance(other, GUID):
return ctypes.cmp(bytes(self), bytes(other))
return -1
def __eq__(self, other): def __eq__(self, other):
return isinstance(other, GUID) and bytes(self) == bytes(other) return isinstance(other, GUID) and bytes(self) == bytes(other)
@ -80,6 +75,10 @@ LPGUID = ctypes.POINTER(GUID)
IID = GUID IID = GUID
REFIID = ctypes.POINTER(IID) REFIID = ctypes.POINTER(IID)
S_OK = 0x00000000
E_NOTIMPL = 0x80004001
E_NOINTERFACE = 0x80004002
class METHOD: class METHOD:
"""COM method.""" """COM method."""
@ -88,244 +87,147 @@ class METHOD:
self.restype = restype self.restype = restype
self.argtypes = args self.argtypes = args
def get_field(self): self.prototype = ctypes.WINFUNCTYPE(self.restype, *self.argtypes)
# ctypes caches WINFUNCTYPE's so this should be ok. self.direct_prototype = ctypes.WINFUNCTYPE(self.restype, ctypes.c_void_p, *self.argtypes)
return ctypes.WINFUNCTYPE(self.restype, *self.argtypes)
def get_com_proxy(self, i, name):
return self.prototype(i, name)
class STDMETHOD(METHOD): class STDMETHOD(METHOD):
"""COM method with HRESULT return value.""" """COM method with HRESULT return value."""
def __init__(self, *args): def __init__(self, *args):
super(STDMETHOD, self).__init__(ctypes.HRESULT, *args) super().__init__(ctypes.HRESULT, *args)
class COMMethodInstance: class VOIDMETHOD(METHOD):
"""Binds a COM interface method.""" """COM method with no return value."""
def __init__(self, name, i, method): def __init__(self, *args):
self.name = name super().__init__(None, *args)
self.i = i
self.method = method
def __get__(self, obj, tp):
if obj is not None:
def _call(*args):
assert _debug_com('COM: #{} IN {}({}, {})'.format(self.i, self.name, obj.__class__.__name__, args))
ret = self.method.get_field()(self.i, self.name)(obj, *args)
assert _debug_com('COM: #{} OUT {}({}, {})'.format(self.i, self.name, obj.__class__.__name__, args))
assert _debug_com('COM: RETURN {}'.format(ret))
return ret
return _call
raise AttributeError()
class COMInterface(ctypes.Structure): _DummyPointerType = ctypes.POINTER(ctypes.c_int)
"""Dummy struct to serve as the type of all COM pointers.""" _PointerMeta = type(_DummyPointerType)
_fields_ = [ _StructMeta = type(ctypes.Structure)
('lpVtbl', ctypes.c_void_p),
]
class InterfacePtrMeta(type(ctypes.POINTER(COMInterface))): class _InterfaceMeta(_StructMeta):
"""Allows interfaces to be subclassed as ctypes POINTER and expects to be populated with data from a COM object. def __new__(cls, name, bases, dct, /, create_pointer_type=True):
TODO: Phase this out and properly use POINTER(Interface) where applicable. if len(bases) > 1:
""" assert _debug_com(f"Ignoring {len(bases) - 1} bases on {name}")
bases = (bases[0],)
if not '_methods_' in dct:
dct['_methods_'] = ()
inh_methods = []
if bases[0] is not ctypes.Structure: # Method does not exist for first definition below
for interface_type in (bases[0].get_interface_inheritance()):
inh_methods.extend(interface_type.__dict__['_methods_'])
inh_methods = tuple(inh_methods)
new_methods = tuple(dct['_methods_'])
vtbl_own_offset = len(inh_methods)
all_methods = tuple(inh_methods) + new_methods
for i, (method_name, mt) in enumerate(all_methods):
assert _debug_com(f"{name}[{i}]: {method_name}: "
f"{(', '.join(t.__name__ for t in mt.argtypes) or 'void')} -> "
f"{'void' if mt.restype is None else mt.restype.__name__}")
vtbl_struct_type = _StructMeta(f"Vtable_{name}",
(ctypes.Structure,),
{'_fields_': [(n, x.direct_prototype) for n, x in all_methods]})
dct['_vtbl_struct_type'] = vtbl_struct_type
dct['vtbl_own_offset'] = vtbl_own_offset
dct['_fields_'] = (('vtbl_ptr', ctypes.POINTER(vtbl_struct_type)),)
res_type = super().__new__(cls, name, bases, dct)
if create_pointer_type:
# If we're not being created from a pInterface subclass as helper Interface (so likely
# being explicitly defined from user code for later use), create the special
# pInterface pointer subclass so it registers itself into the pointer cache
_pInterfaceMeta(f"p{name}", (ctypes.POINTER(bases[0]),), {'_type_': res_type})
return res_type
class _pInterfaceMeta(_PointerMeta):
def __new__(cls, name, bases, dct): def __new__(cls, name, bases, dct):
methods = [] # Interfaces can also be declared by inheritance of pInterface subclasses.
for base in bases[::-1]: # If this happens, create the interface and then become pointer to its struct.
methods.extend(base.__dict__.get('_methods_', ()))
methods.extend(dct.get('_methods_', ()))
for i, (n, method) in enumerate(methods): target = dct.get('_type_', None)
dct[n] = COMMethodInstance(n, i, method) # If we weren't created due to an Interface subclass definition (don't have a _type_),
# just define that Interface subclass from our base's _type_
if target is None:
interface_base = bases[0]._type_
dct['_type_'] = COMInterface # Create corresponding interface type and then set it as target
target = _InterfaceMeta(f"_{name}_HelperInterface",
(interface_base,),
{'_methods_': dct.get('_methods_', ())},
create_pointer_type=False)
dct['_type_'] = target
return super(InterfacePtrMeta, cls).__new__(cls, name, bases, dct) # Create method proxies that will forward ourselves into the interface's methods
for i, (method_name, method) in enumerate(target._methods_):
m = method.get_com_proxy(i + target.vtbl_own_offset, method_name)
def pinterface_method_forward(self, *args, _m=m, _i=i):
assert _debug_com(f'Calling COM {_i} of {target.__name__} ({_m}) through '
f'pointer: ({", ".join(map(repr, (self, *args)))})')
return _m(self, *args)
dct[method_name] = pinterface_method_forward
pointer_type = super().__new__(cls, name, bases, dct)
class pInterface(ctypes.POINTER(COMInterface), metaclass=InterfacePtrMeta): # Hack selves into the ctypes pointer cache so all uses of `ctypes.POINTER` on the
"""Base COM interface pointer.""" # interface type will yield it instead of the inflexible standard pointer type.
# NOTE: This is done pretty much exclusively to help convert COMObjects.
# Some additional work from callers like
class COMInterfaceMeta(type): # RegisterCallback(callback_obj.as_interface(ICallback))
"""This differs in the original as an implemented interface object, not a POINTER object. # instead of
Used when the user must implement their own functions within an interface rather than # RegisterCallback(callback_obj)
being created and generated by the COM object itself. The types are automatically inserted in the ctypes type # could make it obsolete.
cache so it can recognize the type arguments.
"""
def __new__(mcs, name, bases, dct):
methods = dct.pop("_methods_", None)
cls = type.__new__(mcs, name, bases, dct)
if methods is not None:
cls._methods_ = methods
if not bases:
_ptr_bases = (cls, COMPointer)
else:
_ptr_bases = (cls, ctypes.POINTER(bases[0]))
# Class type is dynamically created inside __new__ based on metaclass inheritence; update ctypes cache manually.
from ctypes import _pointer_type_cache from ctypes import _pointer_type_cache
_pointer_type_cache[cls] = type(COMPointer)("POINTER({})".format(cls.__name__), _pointer_type_cache[target] = pointer_type
_ptr_bases,
{"__interface__": cls})
return cls return pointer_type
def __get_subclassed_methodcount(self):
"""Returns the amount of COM methods in all subclasses to determine offset of methods. class Interface(ctypes.Structure, metaclass=_InterfaceMeta, create_pointer_type=False):
Order must be exact from the source when calling COM methods. @classmethod
def get_interface_inheritance(cls):
"""Returns the types of all interfaces implemented by this interface, up to but not
including the base `Interface`.
`Interface` does not represent an actual interface, but merely the base concept of
them, so viewing it as part of an interface's inheritance chain is meaningless.
""" """
try: return cls.__mro__[:cls.__mro__.index(Interface)]
result = 0
for itf in self.mro()[1:-1]:
result += len(itf.__dict__["_methods_"])
return result
except KeyError as err:
(name,) = err.args
if name == "_methods_":
raise TypeError("Interface '{}' requires a _methods_ attribute.".format(itf.__name__))
raise
class COMPointerMeta(type(ctypes.c_void_p), COMInterfaceMeta): class pInterface(_DummyPointerType, metaclass=_pInterfaceMeta):
"""Required to prevent metaclass conflicts with inheritance.""" _type_ = Interface
class COMPointer(ctypes.c_void_p, metaclass=COMPointerMeta):
"""COM Pointer base, could use c_void_p but need to override from_param ."""
@classmethod @classmethod
def from_param(cls, obj): def from_param(cls, obj):
"""Allows obj to return ctypes pointers, even if its base is not a ctype. """When dealing with a COMObject, pry a fitting interface out of it"""
In this case, all we simply want is a ctypes pointer matching the cls interface from the obj.
"""
if obj is None:
return
try: if not isinstance(obj, COMObject):
ptr_dct = obj._pointers return obj
except AttributeError:
raise Exception("Interface method argument specified incorrectly, or passed wrong argument.", cls) return obj.as_interface(cls._type_)
else:
try:
return ptr_dct[cls.__interface__]
except KeyError:
raise TypeError("Interface {} doesn't have a pointer in this class.".format(cls.__name__))
def _missing_impl(interface_name, method_name): class IUnknown(Interface):
"""Functions that are not implemented use this to prevent errors when called."""
def missing_cb_func(*args):
"""Return E_NOTIMPL because the method is not implemented."""
assert _debug_com("Undefined method: {0} was called in interface: {1}".format(method_name, interface_name))
return 0
return missing_cb_func
def _found_impl(interface_name, method_name, method_func):
"""If a method was found in class, we can set it as a callback."""
def cb_func(*args, **kw):
try:
result = method_func(*args, **kw)
except Exception as err:
raise err
if not result: # QOL so callbacks don't need to specify a return for assumed OK's.
return 0
return result
return cb_func
def _make_callback_func(interface, name, method_func):
"""Create a callback function for ctypes if possible."""
if method_func is None:
return _missing_impl(interface, name)
return _found_impl(interface, name, method_func)
# Store structures with same fields to prevent duplicate table creations.
_cached_structures = {}
def create_vtbl_structure(fields, interface):
"""Create virtual table structure with fields for use in COM's."""
try:
return _cached_structures[fields]
except KeyError:
Vtbl = type("Vtbl_{}".format(interface.__name__), (ctypes.Structure,), {"_fields_": fields})
_cached_structures[fields] = Vtbl
return Vtbl
class COMObject:
"""A base class for defining a COM object for use with callbacks and custom implementations."""
_interfaces_ = []
def __new__(cls, *args, **kw):
new_cls = super(COMObject, cls).__new__(cls)
assert len(cls._interfaces_) > 0, "Atleast one interface must be defined to use a COMObject."
new_cls._pointers = {}
new_cls.__create_interface_pointers()
return new_cls
def __create_interface_pointers(cls):
"""Create a custom ctypes structure to handle COM functions in a COM Object."""
interfaces = tuple(cls._interfaces_)
for itf in interfaces[::-1]:
methods = []
fields = []
for interface in itf.__mro__[-2::-1]:
for method in interface._methods_:
name, com_method = method
found_method = getattr(cls, name, None)
mth = _make_callback_func(itf.__name__, name, found_method)
proto = ctypes.WINFUNCTYPE(com_method.restype, *com_method.argtypes)
fields.append((name, proto))
methods.append(proto(mth))
# Make a structure dynamically with the fields given.
itf_structure = create_vtbl_structure(tuple(fields), interface)
# Assign the methods to the fields
vtbl = itf_structure(*methods)
cls._pointers[itf] = ctypes.pointer(ctypes.pointer(vtbl))
@property
def pointers(self):
"""Returns pointers to the implemented interfaces in this COMObject. Read-only.
:type: dict
"""
return self._pointers
class Interface(metaclass=COMInterfaceMeta):
_methods_ = []
class IUnknown(metaclass=COMInterfaceMeta):
"""These methods are not implemented by default yet. Strictly for COM method ordering."""
_methods_ = [ _methods_ = [
('QueryInterface', STDMETHOD(ctypes.c_void_p, REFIID, ctypes.c_void_p)), ('QueryInterface', STDMETHOD(REFIID, ctypes.c_void_p)),
('AddRef', METHOD(ctypes.c_int, ctypes.c_void_p)), ('AddRef', METHOD(ctypes.c_int)),
('Release', METHOD(ctypes.c_int, ctypes.c_void_p)) ('Release', METHOD(ctypes.c_int)),
] ]
@ -333,5 +235,163 @@ class pIUnknown(pInterface):
_methods_ = [ _methods_ = [
('QueryInterface', STDMETHOD(REFIID, ctypes.c_void_p)), ('QueryInterface', STDMETHOD(REFIID, ctypes.c_void_p)),
('AddRef', METHOD(ctypes.c_int)), ('AddRef', METHOD(ctypes.c_int)),
('Release', METHOD(ctypes.c_int)) ('Release', METHOD(ctypes.c_int)),
] ]
def _missing_impl(interface_name, method_name):
"""Create a callback returning E_NOTIMPL for methods not present on a COMObject."""
def missing_cb_func(*_):
assert _debug_com(f"Non-implemented method {method_name} called in {interface_name}")
return E_NOTIMPL
return missing_cb_func
def _found_impl(interface_name, method_name, method_func, self_distance):
"""If a method was found in class, create a callback extracting self from the struct
pointer.
"""
def self_extracting_cb_func(p, *args):
assert _debug_com(f"COMObject method {method_name} called through interface {interface_name}")
self = ctypes.cast(p + self_distance, ctypes.POINTER(ctypes.py_object)).contents.value
result = method_func(self, *args)
# Assume no return statement translates to success
return S_OK if result is None else result
return self_extracting_cb_func
def _adjust_impl(interface_name, method_name, original_method, offset):
"""A method implemented in a previous interface modifies the COMOboject pointer so it
corresponds to an earlier interface and passes it on to the actual implementation.
"""
def adjustor_cb_func(p, *args):
assert _debug_com(f"COMObject method {method_name} called through interface "
f"{interface_name}, adjusting pointer by {offset}")
return original_method(p + offset, *args)
return adjustor_cb_func
class COMObject:
"""A COMObject for implementing C callbacks in Python.
Specify the interface types it supports in `_interfaces_`, and any methods to be implemented
by those interfaces as standard python methods. If the names match, they will be run as
callbacks with all arguments supplied as the types specified in the corresponding interface,
and `self` available as usual.
Remember to call `super().__init__()`.
COMObjects can be passed to ctypes functions directly as long as the corresponding argtype is
an `Interface` pointer, or a `pInterface` subclass.
IUnknown's methods will be autogenerated in case IUnknown is implemented.
"""
def __init_subclass__(cls, /, **kwargs):
super().__init_subclass__(**kwargs)
implemented_leaf_interfaces = cls.__dict__.get('_interfaces_', ())
if not implemented_leaf_interfaces:
raise TypeError("At least one interface must be defined to use a COMObject")
for interface_type in implemented_leaf_interfaces:
for other in implemented_leaf_interfaces:
if interface_type is other:
continue
if issubclass(interface_type, other):
raise TypeError("Only specify the leaf interfaces")
# Sanity check done
_ptr_size = ctypes.sizeof(ctypes.c_void_p)
_vtbl_pointers = []
implemented_methods = {}
# Map all leaf and inherited interfaces to the offset of the vtable containing
# their implementations
_interface_to_vtbl_offset = {}
for i, interface_type in enumerate(implemented_leaf_interfaces):
bases = interface_type.get_interface_inheritance()
for base in bases:
if base not in _interface_to_vtbl_offset:
_interface_to_vtbl_offset[base] = i * _ptr_size
if IUnknown in _interface_to_vtbl_offset:
def QueryInterface(self, iid_ptr, res_ptr):
ctypes.cast(res_ptr, ctypes.POINTER(ctypes.c_void_p))[0] = 0
return E_NOINTERFACE
def AddRef(self):
self._vrefcount += 1
return self._vrefcount
def Release(self):
if self._vrefcount <= 0:
assert _debug_com(
f"COMObject {self}: Release while refcount was {self._vrefcount}"
)
self._vrefcount -= 1
return self._vrefcount
cls.QueryInterface = QueryInterface
cls.AddRef = AddRef
cls.Release = Release
for i, interface_type in enumerate(implemented_leaf_interfaces):
wrappers = []
for method_name, method_type in interface_type._vtbl_struct_type._fields_:
if method_name in implemented_methods:
# Method is already implemented on a previous interface; redirect to it
# See https://devblogs.microsoft.com/oldnewthing/20040206-00/?p=40723
# NOTE: Never tested, might be totally wrong
func, implementing_vtbl_idx = implemented_methods[method_name]
mth = _adjust_impl(interface_type.__name__,
method_name,
func,
(implementing_vtbl_idx - i) * _ptr_size)
else:
if (found_method := getattr(cls, method_name, None)) is None:
mth = _missing_impl(interface_type.__name__, method_name)
else:
mth = _found_impl(interface_type.__name__,
method_name,
found_method,
(len(implemented_leaf_interfaces) - i) * _ptr_size)
implemented_methods[method_name] = (mth, i)
wrappers.append(method_type(mth))
vtbl = interface_type._vtbl_struct_type(*wrappers)
_vtbl_pointers.append(ctypes.pointer(vtbl))
fields = []
for i, itf in enumerate(implemented_leaf_interfaces):
fields.append((f'vtbl_ptr_{i}', ctypes.POINTER(itf._vtbl_struct_type)))
fields.append(('self_', ctypes.py_object))
cls._interface_to_vtbl_offset = _interface_to_vtbl_offset
cls._vtbl_pointers = _vtbl_pointers
cls._struct_type = _StructMeta(f"{cls.__name__}_Struct", (ctypes.Structure,), {'_fields_': fields})
def __init__(self):
self._vrefcount = 1
self._struct = self._struct_type(*self._vtbl_pointers, ctypes.py_object(self))
def as_interface(self, interface_type):
# This method ignores the QueryInterface mechanism completely; no GUIDs are
# associated with Interfaces on the python side, it can't be supported.
# Still works, as so far none of the python-made COMObjects are expected to
# support it by any C code.
# (Also no need to always implement it, some COMObjects do not inherit from IUnknown.)
if (offset := self._interface_to_vtbl_offset.get(interface_type, None)) is None:
raise TypeError(f"Does not implement {interface_type}")
return ctypes.byref(self._struct, offset)

View File

@ -83,15 +83,15 @@ IID_IMMDeviceEnumerator = com.GUID(0xa95664d2, 0x9614, 0x4f35, 0xa7, 0x46, 0xde,
class IMMNotificationClient(com.IUnknown): class IMMNotificationClient(com.IUnknown):
_methods_ = [ _methods_ = [
('OnDeviceStateChanged', ('OnDeviceStateChanged',
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR, DWORD)), com.STDMETHOD(LPCWSTR, DWORD)),
('OnDeviceAdded', ('OnDeviceAdded',
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR)), com.STDMETHOD(LPCWSTR)),
('OnDeviceRemoved', ('OnDeviceRemoved',
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR)), com.STDMETHOD(LPCWSTR)),
('OnDefaultDeviceChanged', ('OnDefaultDeviceChanged',
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, EDataFlow, ERole, LPCWSTR)), com.STDMETHOD(EDataFlow, ERole, LPCWSTR)),
('OnPropertyValueChanged', ('OnPropertyValueChanged',
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR, PROPERTYKEY)), com.STDMETHOD(LPCWSTR, PROPERTYKEY)),
] ]
@ -113,7 +113,7 @@ class AudioNotificationCB(com.COMObject):
self.audio_devices = audio_devices self.audio_devices = audio_devices
self._lost = False self._lost = False
def OnDeviceStateChanged(self, this, pwstrDeviceId, dwNewState): def OnDeviceStateChanged(self, pwstrDeviceId, dwNewState):
device = self.audio_devices.get_cached_device(pwstrDeviceId) device = self.audio_devices.get_cached_device(pwstrDeviceId)
old_state = device.state old_state = device.state
@ -126,17 +126,17 @@ class AudioNotificationCB(com.COMObject):
device.state = dwNewState device.state = dwNewState
self.audio_devices.dispatch_event('on_device_state_changed', device, pyglet_old_state, pyglet_new_state) self.audio_devices.dispatch_event('on_device_state_changed', device, pyglet_old_state, pyglet_new_state)
def OnDeviceAdded(self, this, pwstrDeviceId): def OnDeviceAdded(self, pwstrDeviceId):
dev = self.audio_devices.add_device(pwstrDeviceId) dev = self.audio_devices.add_device(pwstrDeviceId)
assert _debug(f"Audio device was added {pwstrDeviceId}: {dev}") assert _debug(f"Audio device was added {pwstrDeviceId}: {dev}")
self.audio_devices.dispatch_event('on_device_added', dev) self.audio_devices.dispatch_event('on_device_added', dev)
def OnDeviceRemoved(self, this, pwstrDeviceId): def OnDeviceRemoved(self, pwstrDeviceId):
dev = self.audio_devices.remove_device(pwstrDeviceId) dev = self.audio_devices.remove_device(pwstrDeviceId)
assert _debug(f"Audio device was removed {pwstrDeviceId} : {dev}") assert _debug(f"Audio device was removed {pwstrDeviceId} : {dev}")
self.audio_devices.dispatch_event('on_device_removed', dev) self.audio_devices.dispatch_event('on_device_removed', dev)
def OnDefaultDeviceChanged(self, this, flow, role, pwstrDeviceId): def OnDefaultDeviceChanged(self, flow, role, pwstrDeviceId):
# Only support eConsole role right now # Only support eConsole role right now
if role == 0: if role == 0:
if pwstrDeviceId is None: if pwstrDeviceId is None:
@ -149,7 +149,7 @@ class AudioNotificationCB(com.COMObject):
self.audio_devices.dispatch_event('on_default_changed', device, pyglet_flow) self.audio_devices.dispatch_event('on_default_changed', device, pyglet_flow)
def OnPropertyValueChanged(self, this, pwstrDeviceId, key): def OnPropertyValueChanged(self, pwstrDeviceId, key):
pass pass

View File

@ -270,7 +270,6 @@ class IDirectSound(com.pIUnknown):
('Initialize', ('Initialize',
com.STDMETHOD(com.LPGUID)), com.STDMETHOD(com.LPGUID)),
] ]
_type_ = com.COMInterface
DirectSoundCreate = lib.DirectSoundCreate DirectSoundCreate = lib.DirectSoundCreate
DirectSoundCreate.argtypes = \ DirectSoundCreate.argtypes = \

View File

@ -192,17 +192,19 @@ XAUDIO2_NO_VIRTUAL_AUDIO_CLIENT = 0x10000 # Used in CreateMasteringVoice to cr
class IXAudio2VoiceCallback(com.Interface): class IXAudio2VoiceCallback(com.Interface):
_methods_ = [ _methods_ = [
('OnVoiceProcessingPassStart', ('OnVoiceProcessingPassStart',
com.STDMETHOD(UINT32)), com.VOIDMETHOD(UINT32)),
('OnVoiceProcessingPassEnd', ('OnVoiceProcessingPassEnd',
com.STDMETHOD()), com.VOIDMETHOD()),
('onStreamEnd', ('OnStreamEnd',
com.STDMETHOD()), com.VOIDMETHOD()),
('onBufferStart', ('OnBufferStart',
com.STDMETHOD(ctypes.c_void_p)), com.VOIDMETHOD(ctypes.c_void_p)),
('OnBufferEnd', ('OnBufferEnd',
com.STDMETHOD(ctypes.c_void_p)), com.VOIDMETHOD(ctypes.c_void_p)),
('OnLoopEnd', ('OnLoopEnd',
com.STDMETHOD(ctypes.c_void_p)), com.VOIDMETHOD(ctypes.c_void_p)),
('OnVoiceError',
com.VOIDMETHOD(ctypes.c_void_p, HRESULT))
] ]
@ -220,20 +222,9 @@ class XA2SourceCallback(com.COMObject):
_interfaces_ = [IXAudio2VoiceCallback] _interfaces_ = [IXAudio2VoiceCallback]
def __init__(self, xa2_player): def __init__(self, xa2_player):
super().__init__()
self.xa2_player = xa2_player self.xa2_player = xa2_player
def OnVoiceProcessingPassStart(self, bytesRequired):
pass
def OnVoiceProcessingPassEnd(self):
pass
def onStreamEnd(self):
pass
def onBufferStart(self, pBufferContext):
pass
def OnBufferEnd(self, pBufferContext): def OnBufferEnd(self, pBufferContext):
"""At the end of playing one buffer, attempt to refill again. """At the end of playing one buffer, attempt to refill again.
Even if the player is out of sources, it needs to be called to purge all buffers. Even if the player is out of sources, it needs to be called to purge all buffers.
@ -241,10 +232,7 @@ class XA2SourceCallback(com.COMObject):
if self.xa2_player: if self.xa2_player:
self.xa2_player.refill_source_player() self.xa2_player.refill_source_player()
def OnLoopEnd(self, this, pBufferContext): def OnVoiceError(self, pBufferContext, hresult):
pass
def onVoiceError(self, this, pBufferContext, hresult):
raise Exception("Error occurred during audio playback.", hresult) raise Exception("Error occurred during audio playback.", hresult)
@ -362,24 +350,18 @@ class IXAudio2MasteringVoice(IXAudio2Voice):
class IXAudio2EngineCallback(com.Interface): class IXAudio2EngineCallback(com.Interface):
_methods_ = [ _methods_ = [
('OnProcessingPassStart', ('OnProcessingPassStart',
com.METHOD(ctypes.c_void_p)), com.VOIDMETHOD()),
('OnProcessingPassEnd', ('OnProcessingPassEnd',
com.METHOD(ctypes.c_void_p)), com.VOIDMETHOD()),
('OnCriticalError', ('OnCriticalError',
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, ctypes.c_ulong)), com.VOIDMETHOD(HRESULT)),
] ]
class XA2EngineCallback(com.COMObject): class XA2EngineCallback(com.COMObject):
_interfaces_ = [IXAudio2EngineCallback] _interfaces_ = [IXAudio2EngineCallback]
def OnProcessingPassStart(self): def OnCriticalError(self, hresult):
pass
def OnProcessingPassEnd(self):
pass
def OnCriticalError(self, this, hresult):
raise Exception("Critical Error:", hresult) raise Exception("Critical Error:", hresult)

View File

@ -98,7 +98,7 @@ class Caret:
colors = r, g, b, self._visible_alpha, r, g, b, self._visible_alpha colors = r, g, b, self._visible_alpha, r, g, b, self._visible_alpha
self._list = self._group.program.vertex_list(2, gl.GL_LINES, batch, self._group, colors=('Bn', colors)) self._list = self._group.program.vertex_list(2, gl.GL_LINES, self._batch, self._group, colors=('Bn', colors))
self._ideal_x = None self._ideal_x = None
self._ideal_line = None self._ideal_line = None
self._next_attributes = {} self._next_attributes = {}
@ -127,6 +127,7 @@ class Caret:
Also disconnects the caret from further layout events. Also disconnects the caret from further layout events.
""" """
clock.unschedule(self._blink)
self._list.delete() self._list.delete()
self._layout.remove_handlers(self) self._layout.remove_handlers(self)

View File

@ -6,6 +6,7 @@
import time import time
from string import Template
from typing import List from typing import List
from lib_not_dr.types.options import Options from lib_not_dr.types.options import Options
@ -51,11 +52,12 @@ class TimeFormatter(BaseFormatter):
name = 'TimeFormatter' name = 'TimeFormatter'
time_format: str = '%Y-%m-%d %H:%M:%S' time_format: str = '%Y-%m-%d %H:%M:%S'
msec_time_format: str = '{}-{:03d}'
@classmethod @classmethod
def _info(cls) -> str: def _info(cls) -> str:
return cls.add_info('log_time', 'when the log message was created', 'The time format string' return cls.add_info('log_time', 'formatted time when logging', 'The time format string'
'. See https://docs.python.org/3/library/time.html#time.strftime for more information.') '. See https://docs.python.org/3/library/time.html#time.strftime for more information.')
def format(self, message: LogMessage) -> str: def format(self, message: LogMessage) -> str:
return f'[{message.log_time}]' return f'[{message.log_time}]'

View File

@ -2,6 +2,9 @@
# DR basic running from source # DR basic running from source
# DR build (by nuitka) # DR build (by nuitka)
# for function
lib-not-dr
# for images # for images
# not for pypy >= 3.10 # not for pypy >= 3.10
pillow >= 10.0.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython" pillow >= 10.0.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"

View File

@ -3,6 +3,9 @@
# DR build (by nuitka) # DR build (by nuitka)
# DR contributing # DR contributing
# for function
lib-not-dr
# for images # for images
# not for pypy >= 3.10 # not for pypy >= 3.10
pillow >= 10.0.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython" pillow >= 10.0.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"

View File

@ -1,6 +1,9 @@
# this requirement is for # this requirement is for
# DR basic running from source # DR basic running from source
# for function
lib-not-dr
# for images # for images
# not for pypy >= 3.10 # not for pypy >= 3.10
pillow >= 10.0.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython" pillow >= 10.0.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"