diff --git a/src/CoreAudio/AudioToolbox/AUComponent.cpp b/src/CoreAudio/AudioToolbox/AUComponent.cpp index d48eec1ef..1deb304e9 100644 --- a/src/CoreAudio/AudioToolbox/AUComponent.cpp +++ b/src/CoreAudio/AudioToolbox/AUComponent.cpp @@ -22,13 +22,16 @@ along with Darling. If not, see . #include #include #include "ComponentDispatch.h" +#include "../AudioTrace.h" const CFStringRef kAudioComponentRegistrationsChangedNotification = CFSTR("AudioComponentRegistrationsChangedNotification"); const CFStringRef kAudioComponentInstanceInvalidationNotification = CFSTR("AudioComponentInstanceInvalidationNotification"); OSStatus AudioOutputUnitStart(AudioUnit ci) { - return dispatchCall(ci, kAudioOutputUnitStartSelect); + OSStatus status = dispatchCall(ci, kAudioOutputUnitStartSelect); + rclolAudioTrace("UNIT_START unit=%p status=%d\n", (void*) ci, (int) status); + return status; } OSStatus AudioOutputUnitStop(AudioUnit ci) @@ -38,7 +41,9 @@ OSStatus AudioOutputUnitStop(AudioUnit ci) OSStatus AudioUnitInitialize(AudioUnit inUnit) { - return dispatchCall(inUnit, kAudioUnitInitializeSelect); + OSStatus status = dispatchCall(inUnit, kAudioUnitInitializeSelect); + rclolAudioTrace("UNIT_INITIALIZE unit=%p status=%d\n", (void*) inUnit, (int) status); + return status; } OSStatus AudioUnitUninitialize(AudioUnit inUnit) @@ -54,7 +59,9 @@ AudioUnitGetPropertyInfo(AudioUnit inUnit, UInt32 * _Nullable outDataSize, Boolean * _Nullable outWritable) { - return dispatchCall(inUnit, kAudioUnitGetPropertyInfoSelect, inID, inScope, inElement, outDataSize, outWritable); + OSStatus status = dispatchCall(inUnit, kAudioUnitGetPropertyInfoSelect, inID, inScope, inElement, outDataSize, outWritable); + rclolAudioTrace("UNIT_PROPINFO unit=%p prop=%u scope=%u elem=%u status=%d\n", (void*) inUnit, (unsigned) inID, (unsigned) inScope, (unsigned) inElement, (int) status); + return status; } OSStatus @@ -65,7 +72,16 @@ AudioUnitGetProperty(AudioUnit inUnit, void * outData, UInt32 * ioDataSize) { - return dispatchCall(inUnit, kAudioUnitGetPropertySelect, inID, inScope, inElement, outData, ioDataSize); + OSStatus status = dispatchCall(inUnit, kAudioUnitGetPropertySelect, inID, inScope, inElement, outData, ioDataSize); + if (inID == 8 && status == noErr && outData && ioDataSize && *ioDataSize >= sizeof(AudioStreamBasicDescription)) + { + const AudioStreamBasicDescription* d = static_cast(outData); + rclolAudioTrace("UNIT_GOT_FORMAT unit=%p scope=%u elem=%u rate=%.0f id=%08x flags=%08x bpf=%u ch=%u bits=%u\n", + (void*) inUnit, (unsigned) inScope, (unsigned) inElement, d->mSampleRate, (unsigned) d->mFormatID, (unsigned) d->mFormatFlags, + (unsigned) d->mBytesPerFrame, (unsigned) d->mChannelsPerFrame, (unsigned) d->mBitsPerChannel); + } + rclolAudioTrace("UNIT_GET unit=%p prop=%u scope=%u elem=%u status=%d\n", (void*) inUnit, (unsigned) inID, (unsigned) inScope, (unsigned) inElement, (int) status); + return status; } OSStatus @@ -76,7 +92,16 @@ AudioUnitSetProperty(AudioUnit inUnit, const void * _Nullable inData, UInt32 inDataSize) { - return dispatchCall(inUnit, kAudioUnitSetPropertySelect, inID, inScope, inElement, inData, inDataSize); + if (inID == 8 /* kAudioUnitProperty_StreamFormat */ && inData && inDataSize >= sizeof(AudioStreamBasicDescription)) + { + const AudioStreamBasicDescription* d = static_cast(inData); + rclolAudioTrace("UNIT_SET_FORMAT unit=%p scope=%u elem=%u rate=%.0f id=%08x flags=%08x bpp=%u fpp=%u bpf=%u ch=%u bits=%u\n", + (void*) inUnit, (unsigned) inScope, (unsigned) inElement, d->mSampleRate, (unsigned) d->mFormatID, (unsigned) d->mFormatFlags, + (unsigned) d->mBytesPerPacket, (unsigned) d->mFramesPerPacket, (unsigned) d->mBytesPerFrame, (unsigned) d->mChannelsPerFrame, (unsigned) d->mBitsPerChannel); + } + OSStatus status = dispatchCall(inUnit, kAudioUnitSetPropertySelect, inID, inScope, inElement, inData, inDataSize); + rclolAudioTrace("UNIT_SET unit=%p prop=%u scope=%u elem=%u size=%u status=%d\n", (void*) inUnit, (unsigned) inID, (unsigned) inScope, (unsigned) inElement, (unsigned) inDataSize, (int) status); + return status; } OSStatus @@ -103,7 +128,9 @@ AudioUnitAddRenderNotify(AudioUnit inUnit, AURenderCallback inProc, void * _Nullable inProcUserData) { - return dispatchCall(inUnit, kAudioUnitAddRenderNotifySelect, inProc, inProcUserData); + OSStatus status = dispatchCall(inUnit, kAudioUnitAddRenderNotifySelect, inProc, inProcUserData); + rclolAudioTrace("UNIT_ADD_RENDER_NOTIFY unit=%p status=%d\n", (void*) inUnit, (int) status); + return status; } OSStatus @@ -121,7 +148,14 @@ AudioUnitGetParameter(AudioUnit inUnit, AudioUnitElement inElement, AudioUnitParameterValue * outValue) { - return dispatchCall(inUnit, kAudioUnitGetParameterSelect, inID, inScope, inElement, outValue); + OSStatus status = dispatchCall(inUnit, kAudioUnitGetParameterSelect, inID, inScope, inElement, outValue); + { + static unsigned count = 0; + if (rclolAudioTraceEnabled() && (count++ < 200 || count % 500 == 0)) + rclolAudioTrace("UNIT_GET_PARAM unit=%p id=%u scope=%u elem=%u value=%g status=%d\n", (void*) inUnit, + (unsigned) inID, (unsigned) inScope, (unsigned) inElement, (outValue && !status) ? (double) *outValue : 0.0, (int) status); + } + return status; } OSStatus @@ -132,7 +166,14 @@ AudioUnitSetParameter(AudioUnit inUnit, AudioUnitParameterValue inValue, UInt32 inBufferOffsetInFrames) { - return dispatchCall(inUnit, kAudioUnitSetParameterSelect, inID, inScope, inElement, inValue, inBufferOffsetInFrames); + OSStatus status = dispatchCall(inUnit, kAudioUnitSetParameterSelect, inID, inScope, inElement, inValue, inBufferOffsetInFrames); + { + static unsigned count = 0; + if (rclolAudioTraceEnabled() && (count++ < 200 || count % 500 == 0)) + rclolAudioTrace("UNIT_SET_PARAM unit=%p id=%u scope=%u elem=%u value=%g offset=%u status=%d\n", (void*) inUnit, + (unsigned) inID, (unsigned) inScope, (unsigned) inElement, (double) inValue, (unsigned) inBufferOffsetInFrames, (int) status); + } + return status; } OSStatus diff --git a/src/CoreAudio/AudioToolbox/AUGraph.cpp b/src/CoreAudio/AudioToolbox/AUGraph.cpp index ca20b90df..e90b37163 100644 --- a/src/CoreAudio/AudioToolbox/AUGraph.cpp +++ b/src/CoreAudio/AudioToolbox/AUGraph.cpp @@ -1 +1,548 @@ +#include "../AudioTrace.h" +/* +This file is part of Darling. + +Darling is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Darling is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Darling. If not, see . +*/ #include "AUGraph.h" +#include +#include +#include +#include +#include + +// An AUGraph is a set of nodes (audio units created from component descriptions), connections +// between them and render callbacks feeding their inputs. Connections and callbacks are recorded +// while the graph is edited and applied to the audio units when the graph is initialized. + +namespace +{ +struct Node +{ + AUNode id; + AudioComponentDescription description; + AudioUnit unit = nullptr; +}; +} + +struct AUGraphData +{ + std::recursive_mutex mutex; + std::vector nodes; + std::vector connections; + std::vector callbacks; + AUNode nextNode = 1; + bool open = false; + bool initialized = false; + bool running = false; +}; + +namespace +{ +Node* findNode(AUGraphData* graph, AUNode id) +{ + for (Node& node : graph->nodes) + { + if (node.id == id) + return &node; + } + return nullptr; +} + +bool isOutputNode(const Node& node) +{ + return node.description.componentType == kAudioUnitType_Output; +} + +// Undoes whatever initialization has done to the units, keeping the nodes +void uninitializeUnits(AUGraphData* graph) +{ + for (Node& node : graph->nodes) + { + if (node.unit) + AudioUnitUninitialize(node.unit); + } + graph->initialized = false; +} +} + +static OSStatus AUGraphOpenImpl(AUGraph inGraph); +static OSStatus AUGraphConnectNodeInputImpl(AUGraph inGraph, AUNode inSourceNode, UInt32 inSourceOutputNumber, AUNode inDestNode, UInt32 inDestInputNumber); +static OSStatus AUGraphSetNodeInputCallbackImpl(AUGraph inGraph, AUNode inDestNode, UInt32 inDestInputNumber, const AURenderCallbackStruct* inInputCallback); +static OSStatus AUGraphInitializeImpl(AUGraph inGraph); +static OSStatus AUGraphStartImpl(AUGraph inGraph); +OSStatus NewAUGraph(AUGraph* outGraph) +{ + rclolAudioTrace("NEW_GRAPH\n"); + if (!outGraph) + return kAudio_ParamError; + + *outGraph = new AUGraphData; + return noErr; +} + +OSStatus DisposeAUGraph(AUGraph inGraph) +{ + if (!inGraph) + return kAudio_ParamError; + + AUGraphStop(inGraph); + AUGraphUninitialize(inGraph); + AUGraphClose(inGraph); + delete inGraph; + return noErr; +} + +OSStatus AUGraphAddNode(AUGraph inGraph, const AudioComponentDescription* inDescription, AUNode* outNode) +{ + if(inDescription) rclolAudioTrace("ADD_NODE type=%08x subtype=%08x\n",inDescription->componentType,inDescription->componentSubType); + if (!inGraph || !inDescription || !outNode) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + + Node node; + node.id = inGraph->nextNode; + node.description = *inDescription; + + if (inGraph->open) + { + // A node added to an open graph gets its unit right away + AudioComponent component = AudioComponentFindNext(nullptr, inDescription); + OSStatus status = component ? AudioComponentInstanceNew(component, &node.unit) + : kAudioUnitErr_InvalidProperty; + rclolAudioTrace("GRAPH_NODE_UNIT subtype=%08x unit=%p status=%d\n", (unsigned) node.description.componentSubType, (void*) node.unit, (int) status); + if (status != noErr) + return status; + } + + inGraph->nextNode++; + inGraph->nodes.push_back(node); + *outNode = node.id; + return noErr; +} + +OSStatus AUGraphRemoveNode(AUGraph inGraph, AUNode inNode) +{ + if (!inGraph) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + + auto node = std::find_if(inGraph->nodes.begin(), inGraph->nodes.end(), [&](const Node& n) { return n.id == inNode; }); + if (node == inGraph->nodes.end()) + return kAudioUnitErr_InvalidProperty; + + if (node->unit) + { + AudioUnitUninitialize(node->unit); + AudioComponentInstanceDispose(node->unit); + } + inGraph->nodes.erase(node); + + inGraph->connections.erase(std::remove_if(inGraph->connections.begin(), inGraph->connections.end(), + [&](const AUNodeConnection& c) { return c.sourceNode == inNode || c.destNode == inNode; }), inGraph->connections.end()); + inGraph->callbacks.erase(std::remove_if(inGraph->callbacks.begin(), inGraph->callbacks.end(), + [&](const AUNodeRenderCallback& c) { return c.destNode == inNode; }), inGraph->callbacks.end()); + return noErr; +} + +OSStatus AUGraphGetNodeCount(AUGraph inGraph, UInt32* outNumberOfNodes) +{ + if (!inGraph || !outNumberOfNodes) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + *outNumberOfNodes = (UInt32) inGraph->nodes.size(); + return noErr; +} + +OSStatus AUGraphGetIndNode(AUGraph inGraph, UInt32 inIndex, AUNode* outNode) +{ + if (!inGraph || !outNode) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + if (inIndex >= inGraph->nodes.size()) + return kAudio_ParamError; + + *outNode = inGraph->nodes[inIndex].id; + return noErr; +} + +OSStatus AUGraphNodeInfo(AUGraph inGraph, AUNode inNode, AudioComponentDescription* outDescription, AudioUnit* outAudioUnit) +{ + if (!inGraph) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + + Node* node = findNode(inGraph, inNode); + if (!node) + return kAudioUnitErr_InvalidProperty; + + if (outDescription) + *outDescription = node->description; + if (outAudioUnit) + { + // The unit only exists while the graph is open + if (!node->unit) + return kAudioUnitErr_Uninitialized; + *outAudioUnit = node->unit; + } + return noErr; +} + +static OSStatus AUGraphOpenImpl(AUGraph inGraph) +{ + if (!inGraph) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + if (inGraph->open) + return noErr; + + for (Node& node : inGraph->nodes) + { + AudioComponent component = AudioComponentFindNext(nullptr, &node.description); + OSStatus status = component ? AudioComponentInstanceNew(component, &node.unit) + : kAudioUnitErr_InvalidProperty; + rclolAudioTrace("GRAPH_NODE_UNIT subtype=%08x unit=%p status=%d\n", (unsigned) node.description.componentSubType, (void*) node.unit, (int) status); + if (status != noErr) + { + for (Node& created : inGraph->nodes) + { + if (created.unit) + { + AudioComponentInstanceDispose(created.unit); + created.unit = nullptr; + } + } + return status; + } + } + + inGraph->open = true; + return noErr; +} + +OSStatus AUGraphClose(AUGraph inGraph) +{ + if (!inGraph) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + if (inGraph->running) + AUGraphStop(inGraph); + if (inGraph->initialized) + uninitializeUnits(inGraph); + + for (Node& node : inGraph->nodes) + { + if (node.unit) + { + AudioComponentInstanceDispose(node.unit); + node.unit = nullptr; + } + } + inGraph->open = false; + return noErr; +} + +static OSStatus AUGraphConnectNodeInputImpl(AUGraph inGraph, AUNode inSourceNode, UInt32 inSourceOutputNumber, AUNode inDestNode, UInt32 inDestInputNumber) +{ + if (!inGraph) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + if (!findNode(inGraph, inSourceNode) || !findNode(inGraph, inDestNode)) + return kAudioUnitErr_InvalidProperty; + + // An input is fed by one source: a new connection or callback replaces the previous one + AUGraphDisconnectNodeInput(inGraph, inDestNode, inDestInputNumber); + + AUNodeConnection connection; + connection.sourceNode = inSourceNode; + connection.sourceOutputNumber = inSourceOutputNumber; + connection.destNode = inDestNode; + connection.destInputNumber = inDestInputNumber; + inGraph->connections.push_back(connection); + return noErr; +} + +OSStatus AUGraphDisconnectNodeInput(AUGraph inGraph, AUNode inDestNode, UInt32 inDestInputNumber) +{ + if (!inGraph) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + inGraph->connections.erase(std::remove_if(inGraph->connections.begin(), inGraph->connections.end(), + [&](const AUNodeConnection& c) { return c.destNode == inDestNode && c.destInputNumber == inDestInputNumber; }), inGraph->connections.end()); + inGraph->callbacks.erase(std::remove_if(inGraph->callbacks.begin(), inGraph->callbacks.end(), + [&](const AUNodeRenderCallback& c) { return c.destNode == inDestNode && c.destInputNumber == inDestInputNumber; }), inGraph->callbacks.end()); + return noErr; +} + +OSStatus AUGraphClearConnections(AUGraph inGraph) +{ + if (!inGraph) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + inGraph->connections.clear(); + inGraph->callbacks.clear(); + return noErr; +} + +static OSStatus AUGraphSetNodeInputCallbackImpl(AUGraph inGraph, AUNode inDestNode, UInt32 inDestInputNumber, const AURenderCallbackStruct* inInputCallback) +{ + if (!inGraph || !inInputCallback) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + if (!findNode(inGraph, inDestNode)) + return kAudioUnitErr_InvalidProperty; + + AUGraphDisconnectNodeInput(inGraph, inDestNode, inDestInputNumber); + + AUNodeRenderCallback callback; + callback.destNode = inDestNode; + callback.destInputNumber = inDestInputNumber; + callback.cback = *inInputCallback; + inGraph->callbacks.push_back(callback); + return noErr; +} + +OSStatus AUGraphGetNumberOfInteractions(AUGraph inGraph, UInt32* outNumInteractions) +{ + if (!inGraph || !outNumInteractions) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + *outNumInteractions = (UInt32) (inGraph->connections.size() + inGraph->callbacks.size()); + return noErr; +} + +OSStatus AUGraphGetInteractionInfo(AUGraph inGraph, UInt32 inInteractionIndex, AUNodeInteraction* outInteraction) +{ + if (!inGraph || !outInteraction) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + + if (inInteractionIndex < inGraph->connections.size()) + { + outInteraction->nodeInteractionType = 1; // kAUNodeInteraction_Connection + outInteraction->nodeInteraction.connection = inGraph->connections[inInteractionIndex]; + return noErr; + } + + inInteractionIndex -= (UInt32) inGraph->connections.size(); + if (inInteractionIndex < inGraph->callbacks.size()) + { + outInteraction->nodeInteractionType = 2; // kAUNodeInteraction_InputCallback + outInteraction->nodeInteraction.inputCallback = inGraph->callbacks[inInteractionIndex]; + return noErr; + } + return kAudio_ParamError; +} + +static OSStatus AUGraphInitializeImpl(AUGraph inGraph) +{ + if (!inGraph) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + if (!inGraph->open) + return kAudioUnitErr_Uninitialized; + if (inGraph->initialized) + return noErr; + + for (const AUNodeConnection& connection : inGraph->connections) + { + Node* source = findNode(inGraph, connection.sourceNode); + Node* dest = findNode(inGraph, connection.destNode); + if (!source || !dest) + return kAudioUnitErr_InvalidProperty; + + AudioUnitConnection unitConnection; + unitConnection.sourceAudioUnit = source->unit; + unitConnection.sourceOutputNumber = connection.sourceOutputNumber; + unitConnection.destInputNumber = connection.destInputNumber; + + OSStatus status = AudioUnitSetProperty(dest->unit, kAudioUnitProperty_MakeConnection, kAudioUnitScope_Input, + connection.destInputNumber, &unitConnection, sizeof(unitConnection)); + if (status != noErr) + return status; + } + + for (const AUNodeRenderCallback& callback : inGraph->callbacks) + { + Node* dest = findNode(inGraph, callback.destNode); + if (!dest) + return kAudioUnitErr_InvalidProperty; + + OSStatus status = AudioUnitSetProperty(dest->unit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, + callback.destInputNumber, &callback.cback, sizeof(callback.cback)); + if (status != noErr) + return status; + } + + for (Node& node : inGraph->nodes) + { + OSStatus status = AudioUnitInitialize(node.unit); + if (status != noErr) + { + uninitializeUnits(inGraph); + return status; + } + } + + inGraph->initialized = true; + return noErr; +} + +OSStatus AUGraphUninitialize(AUGraph inGraph) +{ + if (!inGraph) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + if (inGraph->running) + AUGraphStop(inGraph); + if (inGraph->initialized) + uninitializeUnits(inGraph); + return noErr; +} + +static OSStatus AUGraphStartImpl(AUGraph inGraph) +{ + if (!inGraph) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + if (!inGraph->initialized) + return kAudioUnitErr_Uninitialized; + if (inGraph->running) + return noErr; + + // Pulling starts at the output units + for (Node& node : inGraph->nodes) + { + if (isOutputNode(node)) + { + OSStatus status = AudioOutputUnitStart(node.unit); + if (status != noErr) + return status; + } + } + + inGraph->running = true; + return noErr; +} + +OSStatus AUGraphStop(AUGraph inGraph) +{ + if (!inGraph) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + if (!inGraph->running) + return noErr; + + for (Node& node : inGraph->nodes) + { + if (node.unit && isOutputNode(node)) + AudioOutputUnitStop(node.unit); + } + + inGraph->running = false; + return noErr; +} + +OSStatus AUGraphIsOpen(AUGraph inGraph, Boolean* outIsOpen) +{ + if (!inGraph || !outIsOpen) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + *outIsOpen = inGraph->open; + return noErr; +} + +OSStatus AUGraphIsInitialized(AUGraph inGraph, Boolean* outIsInitialized) +{ + if (!inGraph || !outIsInitialized) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + *outIsInitialized = inGraph->initialized; + return noErr; +} + +OSStatus AUGraphIsRunning(AUGraph inGraph, Boolean* outIsRunning) +{ + if (!inGraph || !outIsRunning) + return kAudio_ParamError; + + std::lock_guard lock(inGraph->mutex); + *outIsRunning = inGraph->running; + return noErr; +} + +OSStatus AUGraphUpdate(AUGraph inGraph, Boolean* outIsUpdated) +{ + if (!inGraph) + return kAudio_ParamError; + + // Edits are applied when the graph is initialized, so there is never a pending change to apply + if (outIsUpdated) + *outIsUpdated = true; + return noErr; +} + +OSStatus AUGraphOpen(AUGraph inGraph) +{ + OSStatus status = AUGraphOpenImpl(inGraph); + rclolAudioTrace("GRAPH_OPEN status=%d\n", (int) status); + return status; +} + +OSStatus AUGraphConnectNodeInput(AUGraph inGraph, AUNode inSourceNode, UInt32 inSourceOutputNumber, AUNode inDestNode, UInt32 inDestInputNumber) +{ + OSStatus status = AUGraphConnectNodeInputImpl(inGraph, inSourceNode, inSourceOutputNumber, inDestNode, inDestInputNumber); + rclolAudioTrace("GRAPH_CONNECTNODEINPUT status=%d\n", (int) status); + return status; +} + +OSStatus AUGraphSetNodeInputCallback(AUGraph inGraph, AUNode inDestNode, UInt32 inDestInputNumber, const AURenderCallbackStruct* inInputCallback) +{ + OSStatus status = AUGraphSetNodeInputCallbackImpl(inGraph, inDestNode, inDestInputNumber, inInputCallback); + rclolAudioTrace("GRAPH_SETNODEINPUTCALLBACK status=%d\n", (int) status); + return status; +} + +OSStatus AUGraphInitialize(AUGraph inGraph) +{ + OSStatus status = AUGraphInitializeImpl(inGraph); + rclolAudioTrace("GRAPH_INITIALIZE status=%d\n", (int) status); + return status; +} + +OSStatus AUGraphStart(AUGraph inGraph) +{ + OSStatus status = AUGraphStartImpl(inGraph); + rclolAudioTrace("GRAPH_START status=%d\n", (int) status); + return status; +} diff --git a/src/CoreAudio/AudioToolbox/AudioComponentManager.mm b/src/CoreAudio/AudioToolbox/AudioComponentManager.mm index 5fa33817d..8588e69d8 100644 --- a/src/CoreAudio/AudioToolbox/AudioComponentManager.mm +++ b/src/CoreAudio/AudioToolbox/AudioComponentManager.mm @@ -1,3 +1,4 @@ +#include "../AudioTrace.h" /* This file is part of Darling. @@ -18,6 +19,8 @@ along with Darling. If not, see . */ #include "AudioComponentManager.h" #include +#include +#include #include #include @@ -210,6 +213,11 @@ std::vector AudioComponentManager::findMatching(const AudioCompo } } + // Prefer a newer user component over an older system implementation of + // the same audio unit. Retain the existing order for equal versions. + std::stable_sort(rv.begin(),rv.end(),[this](AudioComponent a,AudioComponent b) { + return m_components.at(a).version>m_components.at(b).version; + }); return rv; } @@ -274,6 +282,11 @@ OSStatus AudioComponentManager::instantiate(AudioComponent c, AudioComponentInst return invalidComponentID; RegisteredComponent& regd = it->second; + static unsigned traceCount=0; + if(rclolAudioTraceEnabled() && traceCount++<16) + rclolAudioTrace("AUDIO_COMPONENT version=%u subtype=%08x user_override=%d\n", + regd.version,regd.desc.componentSubType, + regd.bundlePath.find("DarlingRiotAudio.component")!=std::string::npos); if (!regd.factory) { // Load the bundle diff --git a/src/CoreAudio/AudioToolbox/AudioConverter.cpp b/src/CoreAudio/AudioToolbox/AudioConverter.cpp index 202121cd9..5672fcba1 100644 --- a/src/CoreAudio/AudioToolbox/AudioConverter.cpp +++ b/src/CoreAudio/AudioToolbox/AudioConverter.cpp @@ -19,16 +19,39 @@ along with Darling. If not, see . #include "AudioConverter.h" #include "AudioConverterImpl.h" +#include "../AudioTrace.h" #include #include #include +#include +#include +#include +#include + +// Validate opaque-handle ownership before dereferencing a disposal argument. +static std::mutex converterMutex; +static std::unordered_set liveConverters; + +static bool audioTrace() { + static const bool enabled = rclolAudioTraceEnabled(); + return enabled; +} + OSStatus AudioConverterNew(const AudioStreamBasicDescription* inSourceFormat, const AudioStreamBasicDescription* inDestinationFormat, AudioConverterRef* outAudioConverter) { - return AudioConverter::create(inSourceFormat, inDestinationFormat, (AudioConverter**)outAudioConverter); + OSStatus status = AudioConverter::create(inSourceFormat, inDestinationFormat, (AudioConverter**)outAudioConverter); + if (status == noErr && outAudioConverter && *outAudioConverter) { + std::lock_guard lock(converterMutex); + liveConverters.insert(*outAudioConverter); + } + if (audioTrace()) + std::fprintf(stderr, "RIOT_AUDIO new status=%d converter=%p\n", status, + outAudioConverter ? *outAudioConverter : nullptr); + return status; } -OSStatus AudioConverterNewSpecific(const AudioStreamBasicDescription* inSourceFormat, const AudioStreamBasicDescription* inDestinationFormat, UInt32 inNumberClassDescriptions, AudioClassDescription* nClassDescriptions, AudioConverterRef* outAudioConverter) +OSStatus AudioConverterNewSpecific(const AudioStreamBasicDescription* inSourceFormat, const AudioStreamBasicDescription* inDestinationFormat, UInt32 inNumberClassDescriptions, const AudioClassDescription* nClassDescriptions, AudioConverterRef* outAudioConverter) { // FIXME return AudioConverterNew(inSourceFormat, inDestinationFormat, outAudioConverter); @@ -47,6 +70,15 @@ OSStatus AudioConverterReset(AudioConverterRef inAudioConverter) OSStatus AudioConverterDispose(AudioConverterRef inAudioConverter) { + if (audioTrace()) + std::fprintf(stderr, "RIOT_AUDIO dispose converter=%p\n", inAudioConverter); + if (!inAudioConverter) + return paramErr; + { + std::lock_guard lock(converterMutex); + if (!liveConverters.erase(inAudioConverter)) + return paramErr; + } delete (AudioConverter*)inAudioConverter; return noErr; } diff --git a/src/CoreAudio/AudioToolbox/AudioConverterImpl.cpp b/src/CoreAudio/AudioToolbox/AudioConverterImpl.cpp index dca819b06..fc4016e60 100644 --- a/src/CoreAudio/AudioToolbox/AudioConverterImpl.cpp +++ b/src/CoreAudio/AudioToolbox/AudioConverterImpl.cpp @@ -67,6 +67,11 @@ void AudioConverter::flush() OSStatus AudioConverter::create(const AudioStreamBasicDescription* inSourceFormat, const AudioStreamBasicDescription* inDestinationFormat, AudioConverter** out) { TRACE2(inSourceFormat, inDestinationFormat); + if (!out) + return paramErr; + *out = nullptr; + if (!inSourceFormat || !inDestinationFormat) + return paramErr; // TODO: non-interleaved audio @@ -118,6 +123,7 @@ OSStatus AudioConverter::create(const AudioStreamBasicDescription* inSourceForma if (avcodec_open2((*out)->m_decoder, codecIn, nullptr) < 0) { delete *out; + *out = nullptr; std::cerr << "AudioConverter::create(): avcodec_open() failed, format in = " << std::hex << inSourceFormat->mFormatID << ", out = " << inDestinationFormat->mFormatID << std::dec << std::endl; return paramErr; diff --git a/src/CoreAudio/AudioToolbox/ComponentDispatch.h b/src/CoreAudio/AudioToolbox/ComponentDispatch.h index ee6a9b171..8505c3553 100644 --- a/src/CoreAudio/AudioToolbox/ComponentDispatch.h +++ b/src/CoreAudio/AudioToolbox/ComponentDispatch.h @@ -43,7 +43,12 @@ OSStatus dispatchCall(AudioComponentInstance inUnit, SInt16 sel, Args... args) AudioComponentPlugInInterface* iface = AudioComponentManager::instance()->instanceInterface(inUnit); AudioComponentMethod method = iface->Lookup(sel); if (method != nullptr) - return method(iface, args...); + { + // The lookup type is variadic. Calling it directly promotes Float32 + // parameters to double, while audio-unit entry points expect float. + using Method = OSStatus (*)(void*, Args...); + return reinterpret_cast(method)(iface, args...); + } else return badComponentSelector; } diff --git a/src/CoreAudio/CoreAudio/AudioConverterShim.c b/src/CoreAudio/CoreAudio/AudioConverterShim.c new file mode 100644 index 000000000..6e85e8230 --- /dev/null +++ b/src/CoreAudio/CoreAudio/AudioConverterShim.c @@ -0,0 +1,90 @@ +/* Minimal AudioConverter for Darling's CoreAudio framework. + * + * Apple's CoreAudio re-exports these AudioToolbox entry points, and + * third-party code (e.g. Discord voice) binds them against CoreAudio. + * AudioToolbox cannot be re-exported here (it depends on CoreAudio), + * so this TU provides small self-contained implementations: + * - format-identical conversion works (direct frame copy); + * - anything else returns kAudioConverterErr_InvalidInputSize/ + * codec-unavailable rather than crashing the caller at bind time. + */ +#include +#include +#include +#include +#include + +struct DarlingAudioConverter { + AudioStreamBasicDescription src; + AudioStreamBasicDescription dst; +}; + +OSStatus AudioConverterNew(const AudioStreamBasicDescription* inSourceFormat, + const AudioStreamBasicDescription* inDestinationFormat, + AudioConverterRef* outAudioConverter) { + struct DarlingAudioConverter* c; + if (!inSourceFormat || !inDestinationFormat || !outAudioConverter) + return paramErr; + c = (struct DarlingAudioConverter*)malloc(sizeof(*c)); + if (!c) + return memFullErr; + c->src = *inSourceFormat; + c->dst = *inDestinationFormat; + *outAudioConverter = (AudioConverterRef)c; + return noErr; +} + +OSStatus AudioConverterDispose(AudioConverterRef inAudioConverter) { + if (!inAudioConverter) + return paramErr; + free(inAudioConverter); + return noErr; +} + +static int asbd_equal(const AudioStreamBasicDescription* a, + const AudioStreamBasicDescription* b) { + return a->mSampleRate == b->mSampleRate && + a->mFormatID == b->mFormatID && + a->mFormatFlags == b->mFormatFlags && + a->mBytesPerPacket == b->mBytesPerPacket && + a->mFramesPerPacket == b->mFramesPerPacket && + a->mBytesPerFrame == b->mBytesPerFrame && + a->mChannelsPerFrame == b->mChannelsPerFrame && + a->mBitsPerChannel == b->mBitsPerChannel; +} + +OSStatus AudioConverterFillComplexBuffer(AudioConverterRef inAudioConverter, + AudioConverterComplexInputDataProc inInputDataProc, + void* inInputDataProcUserData, + UInt32* ioOutputDataPacketSize, + AudioBufferList* outOutputData, + AudioStreamPacketDescription* outPacketDescription) { + struct DarlingAudioConverter* c = (struct DarlingAudioConverter*)inAudioConverter; + AudioBufferList inBuf; + UInt32 packets, bytes; + OSStatus st; + + (void)outPacketDescription; + if (!c || !inInputDataProc || !ioOutputDataPacketSize || !outOutputData) + return paramErr; + if (!asbd_equal(&c->src, &c->dst)) + return kAudioConverterErr_UnspecifiedError; + if (c->src.mBytesPerPacket == 0 || outOutputData->mNumberBuffers < 1) + return paramErr; + + packets = *ioOutputDataPacketSize; + bytes = packets * c->src.mBytesPerPacket; + if (bytes > outOutputData->mBuffers[0].mDataByteSize) + bytes = outOutputData->mBuffers[0].mDataByteSize; + + inBuf.mNumberBuffers = 1; + inBuf.mBuffers[0].mNumberChannels = c->src.mChannelsPerFrame; + inBuf.mBuffers[0].mDataByteSize = bytes; + inBuf.mBuffers[0].mData = outOutputData->mBuffers[0].mData; + st = inInputDataProc((AudioConverterRef)c, &packets, &inBuf, NULL, inInputDataProcUserData); + if (st != noErr) + return st; + *ioOutputDataPacketSize = packets; + outOutputData->mBuffers[0].mDataByteSize = packets * c->dst.mBytesPerPacket; + return noErr; +} diff --git a/src/CoreAudio/CoreAudio/AudioHardware.cpp b/src/CoreAudio/CoreAudio/AudioHardware.cpp index 7243e6a62..647c8aff7 100644 --- a/src/CoreAudio/CoreAudio/AudioHardware.cpp +++ b/src/CoreAudio/CoreAudio/AudioHardware.cpp @@ -1,3 +1,4 @@ +#include "../AudioTrace.h" /* This file is part of Darling. @@ -25,6 +26,9 @@ along with Darling. If not, see . #include #include #include +#include +#include +#include #include #include "stub.h" @@ -38,7 +42,7 @@ static void initObjects() g_objects.insert(std::make_pair(kAudioObjectSystemObject, std::make_unique(kAudioObjectSystemObject))); g_objects.insert(std::make_pair(kAudioObjectSystemObject + 1, std::make_unique(kAudioObjectSystemObject + 1))); g_objects.insert(std::make_pair(kAudioObjectSystemObject + 2, std::make_unique(kAudioObjectSystemObject + 2))); - g_objects.insert(std::make_pair(kAudioObjectSystemObject + 1, std::make_unique(kAudioObjectSystemObject + 3, "event"))); + g_objects.insert(std::make_pair(kAudioObjectSystemObject + 3, std::make_unique(kAudioObjectSystemObject + 3, "event"))); }); } @@ -58,6 +62,35 @@ static AudioHardwareImpl* GetSystemObject() return GetObject(kAudioObjectSystemObject); } +// Stream IDs are distinct from their owning device IDs. Chromium enumerates +// these objects to determine input/output direction before opening audio. +static OSStatus GetStreamPropertyData(AudioObjectID id, + const AudioObjectPropertyAddress* property, UInt32* size, void* data) +{ + if (!property || !size) return paramErr; + AudioHardwareImpl* device=GetObject(id&0xffffu); + if (!device) return kAudioHardwareBadObjectError; + UInt32 value; + const void* source=&value; + UInt32 required=sizeof(value); + switch(property->mSelector) { + case kAudioStreamPropertyDirection: + value=device->id()==kAudioObjectSystemObject+2?1:0;break; + case kAudioStreamPropertyTerminalType: + value=0;break; + case kAudioStreamPropertyVirtualFormat: + case kAudioStreamPropertyPhysicalFormat: + source=&device->asbd();required=sizeof(AudioStreamBasicDescription);break; + default:return kAudioHardwareUnknownPropertyError; + } + if(data) { + if(*sizemSelector,inAddress->mScope,has); + return has; + } AudioHardwareImpl* obj = GetObject(inObjectID); - if (!obj) + if (!obj) { + rclolAudioTrace("HAS object=%u selector=%08x BADOBJECT\n",inObjectID,inAddress->mSelector); return 0; + } - return obj->hasProperty(inAddress); + bool has=obj->hasProperty(inAddress); + rclolAudioTrace("HAS object=%u selector=%08x scope=%08x elem=%u has=%d\n",inObjectID,inAddress->mSelector,inAddress->mScope,inAddress->mElement,has); + return has; } OSStatus AudioObjectIsPropertySettable(AudioObjectID inObjectID, const AudioObjectPropertyAddress* inAddress, Boolean* outIsSettable) { AudioHardwareImpl* obj = GetObject(inObjectID); - if (!obj) + if (!obj) { + rclolAudioTrace("SETTABLE object=%08x selector=%08x BADOBJECT\n",inObjectID,inAddress->mSelector); return kAudioHardwareBadObjectError; + } - return obj->isPropertySettable(inAddress, outIsSettable); + OSStatus status=obj->isPropertySettable(inAddress, outIsSettable); + rclolAudioTrace("SETTABLE object=%u selector=%08x scope=%08x elem=%u status=%d settable=%d\n",inObjectID,inAddress->mSelector,inAddress->mScope,inAddress->mElement,status,status?0:*outIsSettable); + return status; } OSStatus AudioObjectGetPropertyDataSize(AudioObjectID inObjectID, const AudioObjectPropertyAddress* inAddress, UInt32 inQualifierDataSize, const void* inQualifierData, UInt32* outDataSize) { + if((inObjectID&0xffff0000u)==0x44410000u) { + OSStatus status=GetStreamPropertyData(inObjectID,inAddress,outDataSize,nullptr); + rclolAudioTrace("SIZE stream=%08x selector=%08x scope=%08x status=%d\n",inObjectID,inAddress->mSelector,inAddress->mScope,status); + return status; + } AudioHardwareImpl* obj = GetObject(inObjectID); - if (!obj) + if (!obj) { + rclolAudioTrace("SIZE object=%08x selector=%08x BADOBJECT\n",inObjectID,inAddress->mSelector); return kAudioHardwareBadObjectError; + } - return obj->getPropertyDataSize(inAddress, inQualifierDataSize, - inQualifierData, outDataSize); + OSStatus status=obj->getPropertyDataSize(inAddress,inQualifierDataSize,inQualifierData,outDataSize); + rclolAudioTrace("SIZE object=%u selector=%08x scope=%08x status=%d size=%u\n",inObjectID,inAddress->mSelector,inAddress->mScope,status,status?0:*outDataSize); + return status; } OSStatus AudioObjectGetPropertyData(AudioObjectID inObjectID, const AudioObjectPropertyAddress* inAddress, UInt32 inQualifierDataSize, const void* inQualifierData, UInt32* ioDataSize, void* outData) { + if(inObjectID==kAudioObjectSystemObject && inAddress->mSelector==kAudioHardwarePropertyDeviceForUID) { + if(!outData || !ioDataSize || *ioDataSize(outData); + if(!value->mInputData || value->mInputDataSize!=sizeof(CFStringRef) || !value->mOutputData || value->mOutputDataSize(value->mInputData); + AudioDeviceID found=kAudioObjectUnknown; + AudioObjectPropertyAddress property={kAudioDevicePropertyDeviceUID,kAudioObjectPropertyScopeGlobal,kAudioObjectPropertyElementMaster}; + for(AudioDeviceID id=kAudioObjectSystemObject+1;id<=kAudioObjectSystemObject+3;++id) { + CFStringRef uid=nullptr;UInt32 size=sizeof(uid); + if(GetObject(id)->getPropertyData(&property,0,nullptr,&size,&uid)==noErr && uid) { + bool match=CFEqual(uid,wanted);CFRelease(uid); + if(match) { found=id;break; } + } + } + *static_cast(value->mOutputData)=found; + value->mOutputDataSize=sizeof(found);*ioDataSize=sizeof(*value);return noErr; + } + if((inObjectID&0xffff0000u)==0x44410000u) { + OSStatus status=GetStreamPropertyData(inObjectID,inAddress,ioDataSize,outData); + rclolAudioTrace("GET stream=%08x selector=%08x scope=%08x status=%d\n",inObjectID,inAddress->mSelector,inAddress->mScope,status); + return status; + } AudioHardwareImpl* obj = GetObject(inObjectID); - if (!obj) + if (!obj) { + rclolAudioTrace("GET object=%08x selector=%08x BADOBJECT\n",inObjectID,inAddress->mSelector); return kAudioHardwareBadObjectError; + } - return obj->getPropertyData(inAddress, inQualifierDataSize, inQualifierData, - ioDataSize, outData); + static unsigned traceCount=0; + if(rclolAudioTraceEnabled() && traceCount++<100) + fprintf(stderr,"AUDIO_HARDWARE_GET object=%u property=%08x scope=%08x\n",inObjectID,inAddress->mSelector,inAddress->mScope); + OSStatus status=obj->getPropertyData(inAddress,inQualifierDataSize,inQualifierData,ioDataSize,outData); + rclolAudioTrace("GET object=%u selector=%08x scope=%08x elem=%u status=%d size=%u\n",inObjectID,inAddress->mSelector,inAddress->mScope,inAddress->mElement,status,status?0:*ioDataSize); + return status; } OSStatus AudioObjectSetPropertyData(AudioObjectID inObjectID, @@ -114,11 +196,15 @@ OSStatus AudioObjectSetPropertyData(AudioObjectID inObjectID, const void* inQualifierData, UInt32 inDataSize, const void* inData) { AudioHardwareImpl* obj = GetObject(inObjectID); - if (!obj) + if (!obj) { + rclolAudioTrace("SET object=%08x selector=%08x scope=%08x elem=%u BADOBJECT\n",inObjectID,inAddress->mSelector,inAddress->mScope,inAddress->mElement); return kAudioHardwareBadObjectError; + } - return obj->setPropertyData(inAddress, inQualifierDataSize, inQualifierData, - inDataSize, inData); + OSStatus status=obj->setPropertyData(inAddress,inQualifierDataSize,inQualifierData,inDataSize,inData); + rclolAudioTrace("SET object=%u selector=%08x scope=%08x elem=%u size=%u value=%g status=%d\n",inObjectID,inAddress->mSelector,inAddress->mScope,inAddress->mElement,inDataSize, + (inData && inDataSize==sizeof(Float32))?(double)*(const Float32*)inData:0.0,status); + return status; } OSStatus AudioObjectAddPropertyListener(AudioObjectID inObjectID, @@ -129,7 +215,9 @@ OSStatus AudioObjectAddPropertyListener(AudioObjectID inObjectID, if (!obj) return kAudioHardwareBadObjectError; - return obj->addPropertyListener(inAddress, inListener, inClientData); + OSStatus status=obj->addPropertyListener(inAddress,inListener,inClientData); + rclolAudioTrace("ADD_LISTENER object=%u selector=%08x status=%d\n",inObjectID,inAddress->mSelector,status); + return status; } OSStatus AudioObjectRemovePropertyListener(AudioObjectID inObjectID, diff --git a/src/CoreAudio/CoreAudio/AudioHardwareImpl.cpp b/src/CoreAudio/CoreAudio/AudioHardwareImpl.cpp index f006ba51e..e0e4b4984 100644 --- a/src/CoreAudio/CoreAudio/AudioHardwareImpl.cpp +++ b/src/CoreAudio/CoreAudio/AudioHardwareImpl.cpp @@ -21,6 +21,8 @@ along with Darling. If not, see . #include #include #include +#include +#include #include AudioHardwareImpl::AudioHardwareImpl(AudioObjectID myId) @@ -33,7 +35,7 @@ AudioHardwareImpl::AudioHardwareImpl(AudioObjectID myId) m_asbd.mFormatID = kAudioFormatLinearPCM; m_asbd.mFormatFlags = kLinearPCMFormatFlagIsFloat | kAudioFormatFlagsNativeEndian; m_asbd.mFramesPerPacket = 1; - m_asbd.mBytesPerFrame = UInt32(m_asbd.mBytesPerPacket = m_asbd.mChannelsPerFrame * m_asbd.mSampleRate * m_asbd.mBitsPerChannel / 8); + m_asbd.mBytesPerFrame = UInt32(m_asbd.mBytesPerPacket = m_asbd.mChannelsPerFrame * m_asbd.mBitsPerChannel / 8); } @@ -132,6 +134,9 @@ OSStatus AudioHardwareImpl::isPropertySettable(const AudioObjectPropertyAddress* { switch (inAddress->mSelector) { + case kAudioHardwarePropertyRunLoop: + if(m_myId!=kAudioObjectSystemObject) break; + *outIsSettable=true;return noErr; case kAudioDevicePropertyVolumeScalar: case kAudioDevicePropertyVolumeDecibels: *outIsSettable = true; @@ -151,6 +156,12 @@ OSStatus AudioHardwareImpl::getPropertyData(const AudioObjectPropertyAddress* in { switch (inAddress->mSelector) { + case kAudioHardwarePropertyRunLoop: { + if(m_myId!=kAudioObjectSystemObject) return kAudioHardwareUnknownPropertyError; + if(outData && *ioDataSize(outData); asbd && *ioDataSize >= sizeof(AudioStreamBasicDescription)) @@ -160,6 +171,23 @@ OSStatus AudioHardwareImpl::getPropertyData(const AudioObjectPropertyAddress* in *ioDataSize = sizeof(AudioStreamBasicDescription); return kAudioHardwareNoError; } + case kAudioDevicePropertyBufferFrameSize: { + if(!m_asbd.mBytesPerFrame) return kAudioHardwareBadStreamError; + if(outData && *ioDataSizemMinimum=128;range->mMaximum=2048; } + *ioDataSize=sizeof(AudioValueRange);return noErr; + } + case kAudioDevicePropertyLatency: + case kAudioDevicePropertySafetyOffset: { + if(outData && *ioDataSize(outData); + layout->mChannelLayoutTag=m_asbd.mChannelsPerFrame==2 + ? kAudioChannelLayoutTag_Stereo : kAudioChannelLayoutTag_Mono; + } + *ioDataSize=size;return noErr; + } case kAudioDevicePropertyPreferredChannelsForStereo: { if (UInt32* b = static_cast(outData); outData && *ioDataSize >= sizeof(UInt32)*2) { - b[0] = 0; - b[1] = 1; + b[0] = 1; + b[1] = 2; } *ioDataSize = sizeof(UInt32)*2; return kAudioHardwareNoError; @@ -253,12 +295,26 @@ OSStatus AudioHardwareImpl::getPropertyString(CFStringRef str, UInt32* ioDataSiz OSStatus AudioHardwareImpl::setPropertyData(const AudioObjectPropertyAddress* inAddress, UInt32 inQualifierDataSize, const void* inQualifierData, UInt32 inDataSize, const void* inData) { + if(inAddress->mSelector==kAudioHardwarePropertyRunLoop && m_myId==kAudioObjectSystemObject) { + if(!inData || inDataSize!=sizeof(CFRunLoopRef)) return kAudioHardwareBadPropertySizeError; + // NULL selects HAL-owned threads, which is the PulseAudio backend's + // existing mode. A caller-supplied run loop is not supported yet. + return *(const CFRunLoopRef*)inData ? kAudioHardwareUnsupportedOperationError : noErr; + } + switch (inAddress->mSelector) { case kAudioDevicePropertyBufferSize: { return setBufferSize(*static_cast(inData)); } + case kAudioDevicePropertyBufferFrameSize: { + if(!inData || inDataSize!=sizeof(UInt32)) return kAudioHardwareBadPropertySizeError; + UInt32 frames=*(const UInt32*)inData; + if(frames<128 || frames>2048 || !m_asbd.mBytesPerFrame || + frames>UINT32_MAX/m_asbd.mBytesPerFrame) return paramErr; + return setBufferSize(frames*m_asbd.mBytesPerFrame); + } case kAudioDevicePropertyVolumeScalar: { // TODO: Implement setting the volume @@ -283,7 +339,9 @@ OSStatus AudioHardwareImpl::setPropertyData(const AudioObjectPropertyAddress* in memcpy(&m_asbd, setting, sizeof(m_asbd)); m_asbd.mFramesPerPacket = 1; - m_asbd.mBytesPerFrame = UInt32(m_asbd.mBytesPerPacket = m_asbd.mChannelsPerFrame * m_asbd.mSampleRate * m_asbd.mBitsPerChannel / 8); + m_asbd.mBytesPerFrame = UInt32(m_asbd.mBytesPerPacket = + ((m_asbd.mFormatFlags & kAudioFormatFlagIsNonInterleaved) ? 1 : m_asbd.mChannelsPerFrame) + * m_asbd.mBitsPerChannel / 8); return kAudioHardwareNoError; } diff --git a/src/CoreAudio/CoreAudio/AudioHardwareImpl.h b/src/CoreAudio/CoreAudio/AudioHardwareImpl.h index f74d9b0c3..655e90d51 100644 --- a/src/CoreAudio/CoreAudio/AudioHardwareImpl.h +++ b/src/CoreAudio/CoreAudio/AudioHardwareImpl.h @@ -86,8 +86,7 @@ protected: uint32_t m_bufferSize = 8192; CFStringRef m_name = CFSTR("unknown"), m_uid = CFSTR("unknown"), m_manufacturer = CFSTR("unknown"), m_modelUid = CFSTR("unknown"); - AudioStreamBasicDescription m_asbd; + AudioStreamBasicDescription m_asbd = {}; }; #endif /* AUDIOHARDWAREIMPL_H */ - diff --git a/src/CoreAudio/CoreAudio/CMakeLists.txt b/src/CoreAudio/CoreAudio/CMakeLists.txt index 5f10c2f92..089c94127 100644 --- a/src/CoreAudio/CoreAudio/CMakeLists.txt +++ b/src/CoreAudio/CoreAudio/CMakeLists.txt @@ -8,6 +8,7 @@ set(coreaudio_sources AudioHardware.cpp AudioHardwareImpl.cpp HostTime.mm + AudioConverterShim.c pulse/AudioHardwareImplPA.cpp pulse/AudioHardwareImplPAInput.cpp pulse/AudioHardwareImplPAOutput.cpp diff --git a/src/CoreAudio/CoreAudio/HostTime.mm b/src/CoreAudio/CoreAudio/HostTime.mm index 879fb48a1..fb99e99d1 100644 --- a/src/CoreAudio/CoreAudio/HostTime.mm +++ b/src/CoreAudio/CoreAudio/HostTime.mm @@ -1,8 +1,32 @@ #include "CoreAudio/HostTime.h" #include "stub.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif UInt64 AudioGetCurrentHostTime(void) { - STUB(); - return 0; + return mach_absolute_time(); +} + +UInt64 AudioConvertHostTimeToNanos(UInt64 inHostTime) +{ + static mach_timebase_info_data_t sTimebase = { 0, 0 }; + if (sTimebase.denom == 0) + (void)mach_timebase_info(&sTimebase); + return inHostTime * sTimebase.numer / sTimebase.denom; +} + +UInt64 AudioConvertNanosToHostTime(UInt64 inNanos) +{ + static mach_timebase_info_data_t sTimebase = { 0, 0 }; + if (sTimebase.denom == 0) + (void)mach_timebase_info(&sTimebase); + return inNanos * sTimebase.denom / sTimebase.numer; +} + +#ifdef __cplusplus } +#endif diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.cpp b/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.cpp index 85315086f..c43382686 100644 --- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.cpp +++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.cpp @@ -26,6 +26,8 @@ along with Darling. If not, see . #include #include #include +#include +#include AudioHardwareImplPA::AudioHardwareImplPA(AudioObjectID myId, const char* paRole) : AudioHardwareImpl(myId), m_paRole(paRole) @@ -37,8 +39,34 @@ AudioHardwareImplPA::AudioHardwareImplPA(AudioObjectID myId, const char* paRole) AudioHardwareImplPA::~AudioHardwareImplPA() { - if (m_context) - pa_context_unref(m_context); + for(auto& stream:m_streams) stream.second->stop(); + m_streams.clear(); + if(m_context && m_loop) { + m_loop->sync(^{ pa_context_disconnect(m_context);pa_context_unref(m_context); }); + m_context=nullptr; + } +} + +void AudioHardwareImplPA::syncPA(void (^block)(void)) { + if(m_loop) m_loop->sync(block);else block(); +} + +OSStatus AudioHardwareImplPA::stop(AudioDeviceIOProcID inProcID) { + __block OSStatus result = noErr; + syncPA(^{ + auto it = m_streams.find(inProcID); + if (it == m_streams.end()) { + result = kAudioHardwareNotRunningError; + return; + } + auto* stream = it->second.release(); + m_streams.erase(it); + stream->stop(); + // An IOProc may stop itself. Keep its C++ stream alive until the + // current PulseAudio callback has unwound on this serial queue. + m_loop->async(^{ delete stream; }); + }); + return result; } OSStatus AudioHardwareImplPA::getPropertyData(const AudioObjectPropertyAddress* inAddress, UInt32 inQualifierDataSize, @@ -46,6 +74,18 @@ OSStatus AudioHardwareImplPA::getPropertyData(const AudioObjectPropertyAddress* { switch (inAddress->mSelector) { + case kAudioDevicePropertyStreams: + { + if (m_myId == kAudioObjectSystemObject) return kAudioHardwareUnknownPropertyError; + bool input=m_myId==kAudioObjectSystemObject+2; + bool matches=inAddress->mScope==kAudioObjectPropertyScopeGlobal || + inAddress->mScope==(input?kAudioObjectPropertyScopeInput:kAudioObjectPropertyScopeOutput); + UInt32 size=matches?sizeof(AudioStreamID):0; + if(outData && *ioDataSize(outData); devId && *ioDataSize >= sizeof(AudioDeviceID)) @@ -115,112 +155,47 @@ AudioHardwareStream* AudioHardwareImplPA::createStream(AudioDeviceIOProc callbac extern "C" char ***_NSGetArgv(void); -static void paContextStateCB(pa_context* c, void* priv) -{ - void (^cb)(pa_context*) = (void (^)(pa_context*)) priv; - - pa_context_state_t state = pa_context_get_state(c); - if (state == PA_CONTEXT_READY) - { - // std::cout << "PA_CONTEXT_READY\n"; - - cb(c); - Block_release(cb); - - // FIXME: We won't be notified about later disconnects... - pa_context_set_state_callback(c, nullptr, nullptr); - } - else if (state == PA_CONTEXT_FAILED) - { - std::cerr << "PulseAudio error: PA_CONTEXT_FAILED\n"; - - cb(nullptr); - Block_release(cb); - pa_context_set_state_callback(c, nullptr, nullptr); - } -} - -static const char* appNameFromExecutable() -{ - const char* name = (*_NSGetArgv())[0]; - const char* p = std::strrchr(name, '/'); - - if (p && *(p+1)) - return p+1; - return name; +struct PAContextReady { + std::mutex mutex; + std::condition_variable condition; + bool finished=false; +}; + +static void paContextStateCB(pa_context* context,void* userdata) { + auto state=pa_context_get_state(context); + if(state!=PA_CONTEXT_READY && state!=PA_CONTEXT_FAILED && state!=PA_CONTEXT_TERMINATED) return; + auto* ready=static_cast(userdata); + std::lock_guard lock(ready->mutex); + ready->finished=true; + ready->condition.notify_one(); } -void AudioHardwareImplPA::getPAContext(void (^cb)(pa_context*)) -{ - // TODO: destruction - static std::mutex mutex; - - if (m_context == nullptr) - { - std::unique_lock l(mutex); - - if (m_context == nullptr) - { - const char* appname = appNameFromExecutable(); - const char* appid = "org.darlinghq.some-app"; - - pa_proplist* proplist = pa_proplist_new(); - - // Try to find a better application name & id from the bundle - CFBundleRef mainBundle = CFBundleGetMainBundle(); - if (mainBundle) - { - CFStringRef ident = CFBundleGetIdentifier(mainBundle); - if (ident) - appid = CFStringGetCStringPtr(ident, kCFStringEncodingUTF8); - - CFDictionaryRef infoDict = CFBundleGetLocalInfoDictionary(mainBundle); - if (!infoDict) - infoDict = CFBundleGetInfoDictionary(mainBundle); - - if (infoDict) - { - CFStringRef name = (CFStringRef) CFDictionaryGetValue(infoDict, CFSTR("CFBundleDisplayName")); - if (!name) - name = (CFStringRef) CFDictionaryGetValue(infoDict, CFSTR("CFBundleName")); - if (name) - appname = CFStringGetCStringPtr(name, kCFStringEncodingUTF8); - } - } - - pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, appname); - pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, appid); - if (m_paRole) - pa_proplist_sets(proplist, PA_PROP_MEDIA_ROLE, m_paRole); - // pa_proplist_sets(proplist, PA_PROP_APPLICATION_ICON_NAME, "icon-name"); - // pa_proplist_sets(proplist, PA_PROP_MEDIA_ROLE, "game"); - - m_loop.reset(new PADispatchMainLoop); - - m_context = pa_context_new_with_proplist(m_loop->getAPI(), appname, proplist); - pa_proplist_free(proplist); - - if (!m_context) - { - cb(nullptr); - return; - } - - pa_context_set_state_callback(m_context, paContextStateCB, Block_copy(cb)); - - if (pa_context_connect(m_context, nullptr, PA_CONTEXT_NOFLAGS, nullptr) < 0) - { - std::cerr << "pa_context_connect() returned an error\n"; - pa_context_set_state_callback(m_context, nullptr, nullptr); - cb(nullptr); - return; - } - - m_loop->resume(); - } - } - else - cb(m_context); +void AudioHardwareImplPA::getPAContext(void (^cb)(pa_context*)) { + // Serialize initial connection and reuse. The callback always runs on the + // PulseAudio queue after readiness, with the stream fully constructed. + static std::mutex creationMutex; + std::unique_lock creationLock(creationMutex); + if(!m_context) { + pa_proplist* properties=pa_proplist_new(); + pa_proplist_sets(properties,PA_PROP_APPLICATION_NAME,"Darling CoreAudio"); + pa_proplist_sets(properties,PA_PROP_APPLICATION_ID,"org.darlinghq.coreaudio"); + if(m_paRole) pa_proplist_sets(properties,PA_PROP_MEDIA_ROLE,m_paRole); + m_loop.reset(new PADispatchMainLoop); + m_context=pa_context_new_with_proplist(m_loop->getAPI(),"Darling CoreAudio",properties); + pa_proplist_free(properties); + if(!m_context) { cb(nullptr);return; } + PAContextReady ready; + pa_context_set_state_callback(m_context,paContextStateCB,&ready); + int connected=pa_context_connect(m_context,nullptr,PA_CONTEXT_NOFLAGS,nullptr); + m_loop->resume(); + if(connected>=0) { + std::unique_lock lock(ready.mutex); + ready.condition.wait_for(lock,std::chrono::seconds(10),[&]{return ready.finished;}); + } + // Remove the stack-backed readiness callback before leaving its scope. + m_loop->sync(^{pa_context_set_state_callback(m_context,nullptr,nullptr);}); + } + m_loop->sync(^{cb(pa_context_get_state(m_context)==PA_CONTEXT_READY?m_context:nullptr);}); } pa_sample_spec AudioHardwareImplPA::paSampleSpecForASBD(const AudioStreamBasicDescription& asbd, bool* convertSignedUnsigned) diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.h b/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.h index 8421491cd..03a530bfd 100644 --- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.h +++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.h @@ -38,6 +38,9 @@ public: const void* inQualifierData, UInt32 inDataSize, const void* inData) override; void getPAContext(void (^cb)(pa_context*)); + void syncPA(void (^block)(void)); + dispatch_queue_t paQueue() const { return m_loop ? m_loop->queue() : nullptr; } + OSStatus stop(AudioDeviceIOProcID inProcID) override; static pa_sample_spec paSampleSpecForASBD(const AudioStreamBasicDescription& asbd, bool* convertSignedUnsigned = nullptr); protected: AudioHardwareStream* createStream(AudioDeviceIOProc callback, void* clientData) override; @@ -49,4 +52,3 @@ private: }; #endif - diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.cpp b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.cpp index 0db6ebda7..3f3bd6542 100644 --- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.cpp +++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.cpp @@ -26,6 +26,11 @@ along with Darling. If not, see . AudioHardwareStreamPA::AudioHardwareStreamPA(AudioHardwareImplPA* hw, AudioDeviceIOProc callback, void* clientData) : AudioHardwareStream(hw, false), m_callback(callback), m_clientData(clientData) { +} + +void AudioHardwareStreamPA::initialize() +{ + auto* hw=static_cast(m_hw); hw->getPAContext(^(pa_context* context) { if (!context) { @@ -42,6 +47,7 @@ AudioHardwareStreamPA::AudioHardwareStreamPA(AudioHardwareImplPA* hw, AudioDevic } m_stream = pa_stream_new(context, "CoreAudio", &spec, nullptr); + if(!m_stream) return; //pa_stream_set_state_callback(m_stream, [](pa_stream *s, void *userdata) { //std::cout << "Stream state: " << pa_stream_get_state(s) << std::endl; @@ -59,8 +65,8 @@ AudioHardwareStreamPA::AudioHardwareStreamPA(AudioHardwareImplPA* hw, AudioDevic AudioHardwareStreamPA::~AudioHardwareStreamPA() { - if (m_stream) - pa_stream_unref(m_stream); + auto* hw=static_cast(m_hw); + hw->syncPA(^{if(m_stream) {pa_stream_unref(m_stream);m_stream=nullptr;}}); } void AudioHardwareStreamPA::start() @@ -70,9 +76,20 @@ void AudioHardwareStreamPA::start() void AudioHardwareStreamPA::stop() { - std::unique_lock l(m_stopMutex); - pa_stream_disconnect(m_stream); - m_running = false; + auto* hw=static_cast(m_hw); + hw->syncPA(^{ + // All stream operations and callbacks are serialized by syncPA. + // A callback may call stop() itself; taking its mutex here deadlocks. + m_running=false; + if(m_stream) { + // Disconnect may leave queued protocol work holding a stream ref. + // Detach client callbacks before the C++ owner can be destroyed. + pa_stream_set_write_callback(m_stream,nullptr,nullptr); + pa_stream_set_read_callback(m_stream,nullptr,nullptr); + pa_stream_set_state_callback(m_stream,nullptr,nullptr); + pa_stream_disconnect(m_stream); + } + }); } // This function seems to only convert unsigned to signed, but it works both ways in practice diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.h b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.h index 0425ae97f..fdf08d2dc 100644 --- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.h +++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.h @@ -34,18 +34,17 @@ public: void stop(/*void(^cbDone)()*/) override; protected: + void initialize(); virtual void start(); void transformSignedUnsigned(AudioBufferList* abl) const; protected: AudioDeviceIOProc m_callback; void* m_clientData; - pa_stream* m_stream; - void(^m_cbDone)(); + pa_stream* m_stream = nullptr; + void(^m_cbDone)() = nullptr; bool m_convertSignedUnsigned = false; bool m_running = false; - std::mutex m_stopMutex; }; #endif /* AUDIOHARDWARESTREAMPA_H */ - diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.cpp b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.cpp index d6825f1d5..fd9efc988 100644 --- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.cpp +++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.cpp @@ -21,10 +21,37 @@ along with Darling. If not, see . #include "AudioHardwareImplPA.h" #include #include +#include +#include +#include +#include "../../AudioTrace.h" + +static const mach_timebase_info_data_t& timebase() { + static mach_timebase_info_data_t tb = [] { + mach_timebase_info_data_t t = {0, 0}; + mach_timebase_info(&t); + if (!t.numer || !t.denom) t.numer = t.denom = 1; + return t; + }(); + return tb; +} +static uint64_t hostToNs(uint64_t host) { + return uint64_t((unsigned __int128) host * timebase().numer / timebase().denom); +} +static uint64_t nsToHost(uint64_t ns) { + return uint64_t((unsigned __int128) ns * timebase().denom / timebase().numer); +} +static unsigned g_readEvents = 0; +static bool readShouldTrace() { + if (!rclolAudioTraceEnabled()) return false; + unsigned n = g_readEvents++; + return n < 40 || n % 1000 == 0; +} AudioHardwareStreamPAInput::AudioHardwareStreamPAInput(AudioHardwareImplPA* hw, AudioDeviceIOProc callback, void* clientData) : AudioHardwareStreamPA(hw, callback, clientData) { + initialize(); // m_dump.open("/tmp/dump.raw", std::ios_base::out | std::ios_base::binary); } @@ -32,25 +59,26 @@ void AudioHardwareStreamPAInput::paStreamReadCB(pa_stream* s, size_t length, voi { AudioHardwareStreamPAInput* This = static_cast(self); - std::unique_lock l(This->m_stopMutex); if (!This->m_running) return; - AudioTimeStamp fake = {0}; + const pa_sample_spec* spec = pa_stream_get_sample_spec(This->m_stream); + const size_t frameBytes = spec ? pa_frame_size(spec) : 0; + const double rate = spec ? double(spec->rate) : 0; + if (!frameBytes || rate <= 0) + return; + AudioBufferList* abl = static_cast(alloca(sizeof(AudioBufferList) + sizeof(AudioBuffer))); - std::vector hole; // std::cout << "AudioHardwareStreamPAInput::paStreamReadCB()\n"; while (true) { - abl->mNumberBuffers = 1; - abl->mBuffers[0].mNumberChannels = 2; - - size_t nbytes; + const void* data = nullptr; + size_t nbytes = 0; - int rv = pa_stream_peek(This->m_stream, (const void**) &abl->mBuffers[0].mData, &nbytes); + int rv = pa_stream_peek(This->m_stream, &data, &nbytes); if (rv < 0 || !nbytes) { @@ -58,29 +86,82 @@ void AudioHardwareStreamPAInput::paStreamReadCB(pa_stream* s, size_t length, voi break; } - if (nbytes && !abl->mBuffers[0].mData) + // The fragment at the read index was captured `latency` ago: source latency plus + // everything still unread. Without timing info yet, count only the unread data. + pa_usec_t usec = 0; + int negative = 0; + uint64_t latencyNs; + if (pa_stream_get_latency(This->m_stream, &usec, &negative) == 0) + latencyNs = negative ? 0 : uint64_t(usec) * 1000; + else { - // "If there is a hole, data will be NULL and nbytes will contain the length of the hole." - hole.resize(nbytes); - abl->mBuffers[0].mData = hole.data(); + size_t readable = pa_stream_readable_size(This->m_stream); + latencyNs = uint64_t(pa_bytes_to_usec(std::max(readable, nbytes), spec)) * 1000; } - abl->mBuffers[0].mDataByteSize = nbytes; - - // This->m_dump.write((char*) abl->mBuffers[0].mData, nbytes); - // This->m_dump.flush(); - if (This->m_convertSignedUnsigned) - This->transformSignedUnsigned(abl); + const uint64_t nowHost = mach_absolute_time(); - // std::cout << "AudioHardwareStreamPAInput::paStreamReadCB(): bytes=" << nbytes << std::endl; - OSStatus status = This->m_callback(This->m_hw->id(), &fake, abl, &fake, nullptr, nullptr, This->m_clientData); - - pa_stream_drop(This->m_stream); + const bool hole = !data; + if (hole || This->m_convertSignedUnsigned) + { + // "If there is a hole, data will be NULL and nbytes will contain the length of the hole." + // Holes are delivered as silence so the sample clock keeps counting real time. + // The peeked memory belongs to PulseAudio, so convert a private copy. + This->m_scratch.resize(nbytes); + if (data) + memcpy(This->m_scratch.data(), data, nbytes); + else + memset(This->m_scratch.data(), spec->format == PA_SAMPLE_U8 ? 0x80 : 0, nbytes); + data = This->m_scratch.data(); + } - if (status != noErr) + // Like a real HAL, hand the IOProc at most one device buffer per call. + const size_t block = std::max(frameBytes, This->m_blockBytes); + for (size_t offset = 0; offset + frameBytes <= nbytes; ) { - pa_stream_cork(This->m_stream, true, [](pa_stream*, int, void*) {}, nullptr); - break; + size_t piece = std::min(block, nbytes - offset); + piece -= piece % frameBytes; + + abl->mNumberBuffers = 1; + abl->mBuffers[0].mNumberChannels = spec->channels; + abl->mBuffers[0].mData = const_cast(static_cast(data) + offset); + abl->mBuffers[0].mDataByteSize = piece; + + if (This->m_convertSignedUnsigned) + This->transformSignedUnsigned(abl); + + const uint64_t offsetNs = uint64_t(double(offset / frameBytes) * 1e9 / rate); + const uint64_t agoNs = latencyNs > offsetNs ? latencyNs - offsetNs : 0; + uint64_t inputHost = nowHost - std::min(nowHost, nsToHost(agoNs)); + // Keep host time monotonic even when PA's latency estimate jumps. + if (inputHost <= This->m_lastInputHost) + inputHost = This->m_lastInputHost + 1; + This->m_lastInputHost = inputHost; + + AudioTimeStamp input = {0}; + input.mSampleTime = double(This->m_frames); + input.mHostTime = inputHost; + input.mRateScalar = 1.0; + input.mFlags = kAudioTimeStampHostTimeValid | kAudioTimeStampSampleTimeValid | kAudioTimeStampRateScalarValid; + + AudioTimeStamp now = input; + now.mHostTime = std::max(nowHost, inputHost); + now.mSampleTime = input.mSampleTime + double(hostToNs(now.mHostTime - inputHost)) * rate / 1e9; + + // This->m_dump.write((char*) abl->mBuffers[0].mData, nbytes); + // This->m_dump.flush(); + // A real HAL ignores the IOProc's result; one failed callback must not + // silence the microphone for the rest of the session. + OSStatus status = This->m_callback(This->m_hw->id(), &now, abl, &input, nullptr, nullptr, This->m_clientData); + if (readShouldTrace()) + rclolAudioTrace("PA_IN iop bytes=%zu frames=%llu latencyMs=%.2f hole=%d status=%d\n", piece, + (unsigned long long) This->m_frames, double(latencyNs) / 1e6, hole, (int) status); + + if (!This->m_running) return; + This->m_frames += piece / frameBytes; + offset += piece; } + + pa_stream_drop(This->m_stream); } } @@ -88,10 +169,19 @@ void AudioHardwareStreamPAInput::start() { struct pa_buffer_attr battr; - battr.fragsize = uint32_t(-1); - battr.maxlength = uint32_t(m_bufferSize); + // Record streams only use maxlength and fragsize. Ask for fragments of one device buffer + // (kAudioDevicePropertyBufferFrameSize) for voice latency, and bound the server-side + // backlog so a stalled client can't accumulate seconds of delay. + const uint32_t bpf = m_hw->asbd().mBytesPerFrame; + uint32_t block = bpf ? (m_hw->bufferSize() / bpf) * bpf : m_bufferSize; + if (!block) + block = m_bufferSize; + m_blockBytes = block; + + battr.fragsize = block; + battr.maxlength = block * 16; battr.minreq = uint32_t(-1); - battr.prebuf = battr.maxlength; + battr.prebuf = uint32_t(-1); battr.tlength = uint32_t(-1); // std::cout << "AudioHardwareStreamPAInput::start()\n"; @@ -101,6 +191,9 @@ void AudioHardwareStreamPAInput::start() int rv = pa_stream_connect_record(m_stream, nullptr, &battr, pa_stream_flags_t(PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE) ); + if (rclolAudioTraceEnabled()) + rclolAudioTrace("PA_IN start block=%u rate=%g channels=%u rv=%d\n", block, m_hw->asbd().mSampleRate, + m_hw->asbd().mChannelsPerFrame, rv); // std::cout << "AudioHardwareStreamPAInput::start() rv = " << rv << std::endl; AudioHardwareStreamPA::start(); diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.cpp b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.cpp index 278630f02..54b463c2f 100644 --- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.cpp +++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.cpp @@ -21,71 +21,318 @@ along with Darling. If not, see . #include "AudioHardwareImplPA.h" #include #include +#include +#include "../../AudioTrace.h" + +// Real CoreAudio calls an output IOProc once per IO cycle: evenly spaced at the buffer +// period, with inOutputTime.mHostTime advancing by one buffer duration per call. Clients +// such as Chromium's AUHALStream rely on that (they read the previous renderer buffer and +// request the next one on every call), so PulseAudio's bursty write requests must not +// drive the IOProc directly. Instead a dispatch timer on the PA queue ticks at the block +// period and renders at most one block per tick (plus bounded catch-up after a late wakeup). + +static const unsigned kPrefillBlocks = 8; // == tlength +static const unsigned kMaxCatchUpBlocks = 2; // extra blocks after a late wakeup +static const unsigned kServoWindowTicks = 32; + +static bool rateLimited(unsigned& counter, unsigned head, unsigned every) { + if (!rclolAudioTraceEnabled()) return false; + unsigned n = counter++; + return n < head || n % every == 0; +} +static unsigned g_wcbEvents = 0, g_iopEvents = 0, g_tickEvents = 0, g_paceEvents = 0; +static bool wcbShouldTrace() { return rateLimited(g_wcbEvents, 50, 1000); } +static bool iopShouldTrace() { return rateLimited(g_iopEvents, 200, 500); } +static bool tickShouldTrace() { return rateLimited(g_tickEvents, 200, 500); } +static bool paceShouldTrace() { return rateLimited(g_paceEvents, 100, 100); } + +static const mach_timebase_info_data_t& timebase() { + static mach_timebase_info_data_t tb = [] { + mach_timebase_info_data_t t = {0, 0}; + mach_timebase_info(&t); + if (!t.numer || !t.denom) t.numer = t.denom = 1; + return t; + }(); + return tb; +} +static uint64_t hostToNs(uint64_t host) { + return uint64_t((unsigned __int128) host * timebase().numer / timebase().denom); +} +static uint64_t nsToHost(uint64_t ns) { + return uint64_t((unsigned __int128) ns * timebase().denom / timebase().numer); +} +static uint64_t nowNs() { return hostToNs(mach_absolute_time()); } AudioHardwareStreamPAOutput::AudioHardwareStreamPAOutput(AudioHardwareImplPA* hw, AudioDeviceIOProc callback, void* clientData) : AudioHardwareStreamPA(hw, callback, clientData) { + initialize(); } -void AudioHardwareStreamPAOutput::paStreamWriteCB(pa_stream* s, size_t length, void* self) +AudioHardwareStreamPAOutput::~AudioHardwareStreamPAOutput() { - AudioHardwareStreamPAOutput* This = static_cast(self); - std::unique_lock l(This->m_stopMutex); + // Normally already done by stop(); the timer must never outlive this object. + static_cast(m_hw)->syncPA(^{ m_running = false; cancelTimer(); }); +} - if (!This->m_running) +void AudioHardwareStreamPAOutput::stop() +{ + // Runs on (or synchronously with) the PA queue, so no tick is in flight unless stop() + // was called from inside the IOProc, which renderBlock() handles via m_running. + // Cancellation guarantees the timer handler is never invoked again. + static_cast(m_hw)->syncPA(^{ m_running = false; cancelTimer(); }); + AudioHardwareStreamPA::stop(); +} + +void AudioHardwareStreamPAOutput::cancelTimer() +{ + if (m_timer) { - pa_stream_cork(This->m_stream, true, [](pa_stream*, int, void*) {}, nullptr); - return; + dispatch_source_cancel(m_timer); + dispatch_release(m_timer); + m_timer = nullptr; + } +} + +uint64_t AudioHardwareStreamPAOutput::outputLatencyNs(size_t writable, bool* fromPA) +{ + pa_usec_t usec = 0; + int negative = 0; + if (pa_stream_get_latency(m_stream, &usec, &negative) == 0) + { + *fromPA = true; + return negative ? 0 : uint64_t(usec) * 1000; } + // No timing info yet: at least account for what is queued in the PA buffer. + *fromPA = false; + const pa_buffer_attr* attr = pa_stream_get_buffer_attr(m_stream); + const pa_sample_spec* spec = pa_stream_get_sample_spec(m_stream); + if (!attr || !spec || writable == size_t(-1) || attr->tlength <= writable) + return 0; + return uint64_t(pa_bytes_to_usec(attr->tlength - writable, spec)) * 1000; +} + +AudioHardwareStreamPAOutput::RenderResult AudioHardwareStreamPAOutput::renderBlock(bool trace) +{ + if (!m_running || m_corked || !m_stream || !m_blockBytes) + return RenderResult::Stopped; - // std::cout << "AudioHardwareStreamPAOutput::paStreamWriteCB()\n"; + const size_t writable = pa_stream_writable_size(m_stream); + if (writable == size_t(-1) || writable < m_blockBytes) + return RenderResult::NoSpace; - AudioTimeStamp fake = {0}; AudioBufferList* abl = static_cast(alloca(sizeof(AudioBufferList) + sizeof(AudioBuffer))); + // Non-interleaved (planar) audio would have multiple buffers, but PA doesn't even support that AFAIK + abl->mNumberBuffers = 1; + abl->mBuffers[0].mNumberChannels = m_hw->asbd().mChannelsPerFrame; + + size_t rqsize = m_blockBytes; + if (pa_stream_begin_write(m_stream, (void**)&abl->mBuffers[0].mData, &rqsize) < 0 || !rqsize) + { + if (paceShouldTrace()) rclolAudioTrace("PA_OUT begin_write failed rqsize=%zu\n", rqsize); + return RenderResult::NoSpace; + } + if (rqsize < m_blockBytes) + { + // Never hand the client a partial block (real HALs use a fixed frame count). + if (paceShouldTrace()) rclolAudioTrace("PA_OUT granted rqsize=%zu < block=%zu: cancel\n", rqsize, m_blockBytes); + pa_stream_cancel_write(m_stream); + return RenderResult::NoSpace; + } + abl->mBuffers[0].mDataByteSize = m_blockBytes; + + bool latencyFromPA = false; + const uint64_t latencyNs = outputLatencyNs(writable, &latencyFromPA); + const double rate = m_hw->asbd().mSampleRate; + const double latencyFrames = rate > 0 ? double(latencyNs) * rate / 1e9 : 0; + const uint64_t nowHost = mach_absolute_time(); - size_t done = 0; + AudioTimeStamp now = {0}; + now.mHostTime = nowHost; + // Device position currently being played: the block about to be rendered starts + // latencyFrames after it, matching the host-time difference below. + now.mSampleTime = std::max(0.0, double(m_frames) - latencyFrames); + now.mRateScalar = 1.0; + now.mFlags = kAudioTimeStampHostTimeValid | kAudioTimeStampSampleTimeValid | kAudioTimeStampRateScalarValid; - while (done < length && This->m_running) + AudioTimeStamp output = now; + output.mHostTime = nowHost + nsToHost(latencyNs); + output.mSampleTime = double(m_frames); + + const double dHostMs = m_lastIOProcHost ? double(hostToNs(nowHost - m_lastIOProcHost)) / 1e6 : 0; + m_lastIOProcHost = nowHost; + + OSStatus status = m_callback(m_hw->id(), &now, nullptr, nullptr, abl, &output, m_clientData); + if (trace) + rclolAudioTrace("PA_OUT iop dHostMs=%.3f writable=%zu latencyMs=%.2f(%s) frames=%llu status=%d size=%u\n", + dHostMs, writable, double(latencyNs) / 1e6, latencyFromPA ? "pa" : "est", + (unsigned long long) m_frames, (int) status, (unsigned) abl->mBuffers[0].mDataByteSize); + + if (!m_running) { - // Non-interleaved (planar) audio would have multiple buffers, but PA doesn't even support that AFAIK - abl->mNumberBuffers = 1; + // The client stopped/disconnected the stream during its IOProc. + pa_stream_cancel_write(m_stream); + return RenderResult::Stopped; + } + if (status != noErr || !abl->mBuffers[0].mDataByteSize) + { + if (paceShouldTrace()) rclolAudioTrace("PA_OUT IOProc failed/empty status=%d size=%u: CORK\n", (int) status, + (unsigned) abl->mBuffers[0].mDataByteSize); + pa_stream_cancel_write(m_stream); + pa_stream_cork(m_stream, true, [](pa_stream*, int, void*) {}, nullptr); + m_corked = true; + return RenderResult::Stopped; + } - abl->mBuffers[0].mNumberChannels = 2; - // abl->mBuffers[0].mData = This->m_buffer; + if (m_convertSignedUnsigned) + transformSignedUnsigned(abl); - // std::cout << "AudioHardwareStreamPAOutput() length req=" << length << ", done=" << done << std::endl; - size_t rqsize = std::min(This->m_bufferSize, length-done); - pa_stream_begin_write(This->m_stream, (void**) &abl->mBuffers[0].mData, &rqsize); + // Always submit exactly one block so the sample clock stays block-aligned. + int rv = pa_stream_write(m_stream, abl->mBuffers[0].mData, m_blockBytes, nullptr, 0, PA_SEEK_RELATIVE); + if (rv != 0) + { + if (paceShouldTrace()) rclolAudioTrace("PA_OUT pa_stream_write rv=%d\n", rv); + return RenderResult::NoSpace; + } + m_frames += m_blockFrames; + return RenderResult::Written; +} - abl->mBuffers[0].mDataByteSize = rqsize; +uint64_t AudioHardwareStreamPAOutput::deadlineNs() const +{ + return m_anchorNs + uint64_t(std::max(0.0, double(m_tickIndex) * m_periodNs - m_advanceNs)); +} - // Call the client for more data - OSStatus status = This->m_callback(This->m_hw->id(), &fake, nullptr, nullptr, abl, &fake, This->m_clientData); - if (status != noErr || !abl->mBuffers[0].mDataByteSize) - { - // std::cerr << "AudioDeviceIOProc returned " << status << ", corking...\n"; +void AudioHardwareStreamPAOutput::armTimer(uint64_t now) +{ + if (!m_timer) + return; + const uint64_t deadline = deadlineNs(); + const int64_t delta = deadline > now ? int64_t(deadline - now) : 0; + dispatch_source_set_timer(m_timer, dispatch_time(DISPATCH_TIME_NOW, delta), DISPATCH_TIME_FOREVER, 100 * NSEC_PER_USEC); +} - pa_stream_cork(This->m_stream, true, [](pa_stream*, int, void*) {}, nullptr); - break; - } - else - { - if (This->m_convertSignedUnsigned) - This->transformSignedUnsigned(abl); - - // std::cout << "AudioHardwareStreamPAOutput::paStreamWriteCB(): got " << abl->mBuffers[0].mDataByteSize << " bytes\n"; - int rv = pa_stream_write(This->m_stream, abl->mBuffers[0].mData, abl->mBuffers[0].mDataByteSize, nullptr, 0, PA_SEEK_RELATIVE); - if (rv != 0) - { - // std::cerr << "pa_stream_write() failed\n"; - break; - } - //else - // std::cout << "pa_stream_write() OK\n"; - } +void AudioHardwareStreamPAOutput::prefill() +{ + if (!m_running || m_corked || m_primed || !m_stream || pa_stream_get_state(m_stream) != PA_STREAM_READY) + return; + + // The only burst: fill the target buffer once at start. + unsigned blocks = 0; + RenderResult r = RenderResult::Written; + while (blocks < kPrefillBlocks && (r = renderBlock(iopShouldTrace())) == RenderResult::Written) + ++blocks; + if (rclolAudioTraceEnabled()) + rclolAudioTrace("PA_OUT prefill blocks=%u result=%d periodMs=%.3f blockFrames=%u\n", blocks, (int) r, + m_periodNs / 1e6, m_blockFrames); + if (r == RenderResult::Stopped || !blocks) + return; + + m_primed = true; + m_anchorNs = nowNs(); + m_tickIndex = 1; + m_advanceNs = 0; + armTimer(m_anchorNs); +} + +void AudioHardwareStreamPAOutput::tick() +{ + if (!m_running || m_corked || !m_timer) + return; + if (!m_primed) + { + prefill(); + return; + } + + uint64_t now = nowNs(); + uint64_t deadline = deadlineNs(); + if (now < deadline) + { + // Early wakeup (leeway); wait for the real deadline. + armTimer(now); + return; + } + + const bool trace = tickShouldTrace(); + const size_t writable = pa_stream_writable_size(m_stream); + if (writable != size_t(-1)) + m_windowMinWritable = std::min(m_windowMinWritable, writable); + + const uint64_t behind = uint64_t(double(now - deadline) / m_periodNs); + unsigned toRender = 1; + if (behind > kMaxCatchUpBlocks) + { + // Far behind (stalled queue, suspended process): don't burst, restart the clock. + if (paceShouldTrace()) + rclolAudioTrace("PA_OUT reanchor lateMs=%.3f behindBlocks=%llu\n", double(now - deadline) / 1e6, + (unsigned long long) behind); + m_anchorNs = now; + m_tickIndex = 0; + m_advanceNs = 0; + } + else if (behind) + { + toRender += unsigned(behind); + if (paceShouldTrace()) + rclolAudioTrace("PA_OUT catchup lateMs=%.3f blocks=%u\n", double(now - deadline) / 1e6, toRender); + } + + if (trace) + rclolAudioTrace("PA_OUT tick lateMs=%.3f writable=%zu render=%u\n", double(now - deadline) / 1e6, writable, toRender); + + for (unsigned i = 0; i < toRender; i++) + { + RenderResult r = renderBlock(iopShouldTrace()); + if (r == RenderResult::Stopped) + return; // also covers stop() from inside the IOProc (timer already cancelled) + if (r == RenderResult::NoSpace && paceShouldTrace()) + rclolAudioTrace("PA_OUT skip: writable=%zu < block=%zu\n", pa_stream_writable_size(m_stream), m_blockBytes); + // The IO cycle elapses either way; a skipped cycle means PA is consuming slower than us. + ++m_tickIndex; + } - done += abl->mBuffers[0].mDataByteSize; + // Drift servo: the PA sink clock isn't our clock. If the buffer stayed under-filled for a + // whole window (min writable >= 2 blocks), run ~6% faster for the next window. Each tick + // stays a single, evenly spaced IOProc call. + if (++m_windowTicks >= kServoWindowTicks) + { + const bool low = m_windowMinWritable != SIZE_MAX && m_windowMinWritable >= 2 * m_blockBytes; + const double step = low ? m_periodNs / 16 : 0; + if (step != m_servoStepNs && paceShouldTrace()) + rclolAudioTrace("PA_OUT servo minWritable=%zu step=%s\n", m_windowMinWritable, low ? "fast" : "off"); + m_servoStepNs = step; + m_windowMinWritable = SIZE_MAX; + m_windowTicks = 0; } + m_advanceNs += m_servoStepNs; + + armTimer(nowNs()); +} + +void AudioHardwareStreamPAOutput::paStreamWriteCB(pa_stream* s, size_t length, void* self) +{ + AudioHardwareStreamPAOutput* This = static_cast(self); + + if (!This->m_running) + { + if (wcbShouldTrace()) rclolAudioTrace("PA_WRITE_CB length=%zu not-running: corking\n", length); + pa_stream_cork(This->m_stream, true, [](pa_stream*, int, void*) {}, nullptr); + return; + } + + // Rendering is paced by the IO timer; the write request only triggers the initial prefill. + if (wcbShouldTrace()) + rclolAudioTrace("PA_WRITE_CB length=%zu writable=%zu primed=%d corked=%d\n", length, + pa_stream_writable_size(s), This->m_primed, This->m_corked); + if (!This->m_timer) + { + // No pacing timer could be created: fall back to rendering whole blocks on demand. + while (This->renderBlock(iopShouldTrace()) == RenderResult::Written) {} + return; + } + if (!This->m_primed) + This->prefill(); } void AudioHardwareStreamPAOutput::start() @@ -93,10 +340,44 @@ void AudioHardwareStreamPAOutput::start() struct pa_buffer_attr battr; battr.fragsize = uint32_t(-1); - battr.maxlength = uint32_t(m_bufferSize); - battr.minreq = uint32_t(-1); - battr.prebuf = battr.maxlength; - battr.tlength = uint32_t(-1); + // Size the server buffer in whole IOProc blocks so every IO cycle can supply one. + uint32_t bpf = m_hw->asbd().mBytesPerFrame; + uint32_t block = bpf ? (m_hw->bufferSize() / bpf) * bpf : m_bufferSize; + if (!block) + block = m_bufferSize; + // ~46 ms target at 256 frames/44.1 kHz: a 2-block target underran, since each + // block waits on a cross-process round trip to the client's renderer. + battr.maxlength = block * 16; + battr.minreq = block; + battr.prebuf = block * 2; + battr.tlength = block * kPrefillBlocks; + + // Real HALs always call the IOProc with the same frame count (kAudioDevicePropertyBufferFrameSize). + m_blockFrames = bpf ? m_hw->bufferSize() / bpf : 0; + m_blockBytes = size_t(m_blockFrames) * bpf; + const double rate = m_hw->asbd().mSampleRate; + m_periodNs = rate > 0 ? double(m_blockFrames) * 1e9 / rate : 0; + + // start() runs on the PA queue, so the timer shares its serialization with all pa_* calls. + // Disabled: Darling's dispatch timers fire 3-35 ms late (one stalled 30 s), which starved + // PA into silence. Render on PA's write requests instead; the output timestamps still + // come from PA's latency, so blocks within one request get correctly spaced play times. + static const bool kUsePacingTimer = false; + dispatch_queue_t queue = static_cast(m_hw)->paQueue(); + if (kUsePacingTimer && queue && m_blockBytes && m_periodNs > 0) + { + m_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); + if (m_timer) + { + dispatch_source_set_event_handler(m_timer, ^{ tick(); }); + // Resumed immediately (a suspended source can't be released); armed after prefill. + dispatch_source_set_timer(m_timer, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(m_timer); + } + } + if (rclolAudioTraceEnabled()) + rclolAudioTrace("PA_OUT start block=%zu frames=%u periodMs=%.3f timer=%d\n", m_blockBytes, m_blockFrames, + m_periodNs / 1e6, m_timer != nullptr); pa_stream_set_write_callback(m_stream, paStreamWriteCB, this); @@ -105,7 +386,5 @@ void AudioHardwareStreamPAOutput::start() |PA_STREAM_AUTO_TIMING_UPDATE) /* PA_STREAM_START_CORKED */, nullptr, nullptr); - // pa_stream_cork(m_stream, false, [](pa_stream*, int, void*) {}, nullptr); AudioHardwareStreamPA::start(); } - diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.h b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.h index 735474128..8d73f9902 100644 --- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.h +++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.h @@ -20,15 +20,46 @@ along with Darling. If not, see . #ifndef AUDIOHARDWARESTREAMPAOUTPUT_H #define AUDIOHARDWARESTREAMPAOUTPUT_H #include "AudioHardwareStreamPA.h" +#include +#include class AudioHardwareStreamPAOutput : public AudioHardwareStreamPA { public: AudioHardwareStreamPAOutput(AudioHardwareImplPA* hw, AudioDeviceIOProc callback, void* clientData); + ~AudioHardwareStreamPAOutput(); + void stop() override; private: + enum class RenderResult { Written, NoSpace, Stopped }; + + uint64_t m_frames = 0; void start() override; static void paStreamWriteCB(pa_stream* s, size_t length, void* self); + + // IO cycle pacing; everything below is only touched on the PA queue. + void cancelTimer(); + void prefill(); + void tick(); + void armTimer(uint64_t nowNs); + uint64_t deadlineNs() const; + RenderResult renderBlock(bool trace); + uint64_t outputLatencyNs(size_t writable, bool* fromPA); + + dispatch_source_t m_timer = nullptr; + size_t m_blockBytes = 0; + uint32_t m_blockFrames = 0; + double m_periodNs = 0; + bool m_primed = false; + bool m_corked = false; + // Deadline k is m_anchorNs + k * m_periodNs - m_advanceNs. + uint64_t m_anchorNs = 0; + uint64_t m_tickIndex = 0; + double m_advanceNs = 0; + // Drift servo: smallest writable size seen over the current window of ticks. + size_t m_windowMinWritable = SIZE_MAX; + unsigned m_windowTicks = 0; + double m_servoStepNs = 0; + uint64_t m_lastIOProcHost = 0; }; #endif /* AUDIOHARDWARESTREAMPA_H */ - diff --git a/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.cpp b/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.cpp index 63f3545e2..4a2d8ed62 100644 --- a/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.cpp +++ b/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.cpp @@ -23,6 +23,8 @@ PADispatchMainLoop::PADispatchMainLoop() { // Create GCD queue m_queue = dispatch_queue_create("org.darlinghq.coreaudio.pulseaudio", nullptr); + m_sources=dispatch_group_create(); + dispatch_queue_set_specific(m_queue,this,this,nullptr); // PulseAudio expects the event loop to be suspended while we're creating the context. // I couldn't find any mention of this in the documentation, but figured it out the hard way. @@ -32,9 +34,22 @@ PADispatchMainLoop::PADispatchMainLoop() PADispatchMainLoop::~PADispatchMainLoop() { resume(); + dispatch_group_wait(m_sources,DISPATCH_TIME_FOREVER); + dispatch_sync(m_queue, ^{}); + dispatch_release(m_sources); dispatch_release(m_queue); } +void PADispatchMainLoop::sync(void (^block)(void)) { + if(dispatch_get_specific(this)==this) block(); + else {resume();dispatch_sync(m_queue,block);} +} + +void PADispatchMainLoop::async(void (^block)(void)) { + resume(); + dispatch_async(m_queue, block); +} + void PADispatchMainLoop::suspend() { if (!m_suspended) @@ -87,6 +102,8 @@ struct dual_source pa_io_event_flags_t events; void* userdata; int fd; + bool freed=false; + unsigned cancellations=2; }; pa_io_event* PADispatchMainLoop::io_new(pa_mainloop_api *a, int fd, pa_io_event_flags_t events, pa_io_event_cb_t cb, void *userdata) @@ -108,22 +125,25 @@ pa_io_event* PADispatchMainLoop::io_new(pa_mainloop_api *a, int fd, pa_io_event_ dual->sourceWrite = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, fd, 0, q); dispatch_source_set_event_handler(dual->sourceRead, ^{ - dual->callback(This->getAPI(), reinterpret_cast(dual), fd, PA_IO_EVENT_INPUT, dual->userdata); + if(!dual->freed && dual->readResumed) dual->callback(This->getAPI(), reinterpret_cast(dual), fd, PA_IO_EVENT_INPUT, dual->userdata); }); dispatch_source_set_event_handler(dual->sourceWrite, ^{ // std::cout << "PADispatchMainLoop::io_new(): write event on fd " << fd << std::endl; - dual->callback(This->getAPI(), reinterpret_cast(dual), fd, PA_IO_EVENT_OUTPUT, dual->userdata); + if(!dual->freed && dual->writeResumed) dual->callback(This->getAPI(), reinterpret_cast(dual), fd, PA_IO_EVENT_OUTPUT, dual->userdata); }); - dispatch_source_set_cancel_handler(dual->sourceWrite, ^{ - if (dual->destroy) - { - dual->destroy(This->getAPI(), reinterpret_cast(dual), dual->userdata); - dual->destroy = nullptr; - } - delete dual; - }); + dispatch_group_enter(This->m_sources); + dispatch_group_enter(This->m_sources); + void (^cancel)(void)=^{ + if(--dual->cancellations==0) { + if(dual->destroy) dual->destroy(This->getAPI(),reinterpret_cast(dual),dual->userdata); + delete dual; + } + dispatch_group_leave(This->m_sources); + }; + dispatch_source_set_cancel_handler(dual->sourceRead,cancel); + dispatch_source_set_cancel_handler(dual->sourceWrite,cancel); if (events & (PA_IO_EVENT_INPUT | PA_IO_EVENT_ERROR | PA_IO_EVENT_HANGUP)) { @@ -185,6 +205,7 @@ void PADispatchMainLoop::io_free(pa_io_event *e) { dual_source* dual = reinterpret_cast(e); // std::cout << "PADispatchMainLoop::io_free(): fd=" << dual->fd << std::endl; + dual->freed=true; if (!dual->readResumed) dispatch_resume(dual->sourceRead); @@ -218,6 +239,7 @@ struct pa_timer struct timespec when; struct linux_timeval tv; pa_time_event_destroy_cb_t destroy = nullptr; + bool freed=false; }; pa_time_event *PADispatchMainLoop::time_new(pa_mainloop_api *a, const struct timeval *tv, pa_time_event_cb_t cb, void *userdata) @@ -232,22 +254,24 @@ pa_time_event *PADispatchMainLoop::time_new(pa_mainloop_api *a, const struct tim timer->source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q); - timer->when.tv_sec = real_tv->tv_sec; - timer->when.tv_nsec = real_tv->tv_usec * 1000; // convert to ns - timer->tv = *real_tv; + timer->when.tv_sec = real_tv ? real_tv->tv_sec : 0; + timer->when.tv_nsec = real_tv ? real_tv->tv_usec * 1000 : 0; + timer->tv = real_tv ? *real_tv : linux_timeval{}; // std::cout << "PADispatchMainLoop::time_new(), sec=" << real_tv->tv_sec << ", usec=" << real_tv->tv_usec << std::endl; - dispatch_source_set_timer(timer->source, dispatch_walltime(&timer->when, 0), 0, 0); + dispatch_source_set_timer(timer->source, real_tv ? dispatch_walltime(&timer->when, 0) : DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 0); dispatch_source_set_event_handler(timer->source, ^{ // std::cout << "PADispatchMainLoop::time_new(): fired\n"; - cb(This->getAPI(), reinterpret_cast(timer), reinterpret_cast(&timer->tv), timer->userdata); + if(!timer->freed) cb(This->getAPI(), reinterpret_cast(timer), reinterpret_cast(&timer->tv), timer->userdata); }); + dispatch_group_enter(This->m_sources); dispatch_source_set_cancel_handler(timer->source, ^{ if (timer->destroy) timer->destroy(This->getAPI(), reinterpret_cast(timer), timer->userdata); delete timer; + dispatch_group_leave(This->m_sources); }); dispatch_resume(timer->source); @@ -259,6 +283,7 @@ void PADispatchMainLoop::time_restart(pa_time_event *e, const struct timeval *tv { const linux_timeval* real_tv = reinterpret_cast(tv); pa_timer* timer = reinterpret_cast(e); + if(!tv) { dispatch_source_set_timer(timer->source,DISPATCH_TIME_FOREVER,DISPATCH_TIME_FOREVER,0);return; } timer->when.tv_sec = real_tv->tv_sec; timer->when.tv_nsec = real_tv->tv_usec * 1000; // convert to ns @@ -266,7 +291,7 @@ void PADispatchMainLoop::time_restart(pa_time_event *e, const struct timeval *tv // std::cout << "PADispatchMainLoop::time_restart(), sec=" << real_tv->tv_sec << ", usec=" << real_tv->tv_usec << std::endl; - dispatch_source_set_timer(timer->source, dispatch_walltime(&timer->when, 0), 0, 0); + dispatch_source_set_timer(timer->source, dispatch_walltime(&timer->when, 0), DISPATCH_TIME_FOREVER, 0); } void PADispatchMainLoop::time_set_destroy(pa_time_event *e, pa_time_event_destroy_cb_t cb) @@ -281,6 +306,7 @@ void PADispatchMainLoop::time_free(pa_time_event *e) pa_timer* timer = reinterpret_cast(e); // std::cout << "PADispatchMainLoop::time_free()\n"; + timer->freed=true; dispatch_source_cancel(timer->source); dispatch_release(timer->source); @@ -289,6 +315,8 @@ void PADispatchMainLoop::time_free(pa_time_event *e) struct defer_event { bool active = true; + bool scheduled = true; + bool freed = false; void* userdata; pa_defer_event_cb_t callback; pa_defer_event_destroy_cb_t destroy = nullptr; @@ -298,7 +326,8 @@ struct defer_event void PADispatchMainLoop::defer_event_fire(void* context) { defer_event* ev = static_cast(context); - if (ev->active) + ev->scheduled=false; + if (ev->active && !ev->freed) { pa_mainloop_api* a = ev->loop->getAPI(); dispatch_queue_t q = static_cast(a->userdata)->m_queue; @@ -307,8 +336,10 @@ void PADispatchMainLoop::defer_event_fire(void* context) ev->callback(a, static_cast(context), ev->userdata); - if (ev->active) - dispatch_async_f(q, context, defer_event_fire); + if(ev->active && !ev->freed && !ev->scheduled) { + ev->scheduled=true; + dispatch_async_f(q,context,defer_event_fire); + } } } @@ -322,7 +353,7 @@ pa_defer_event *PADispatchMainLoop::defer_new(pa_mainloop_api *a, pa_defer_event e->loop = This; e->userdata = userdata; e->callback = cb; - + dispatch_group_enter(This->m_sources); dispatch_async_f(q, e, defer_event_fire); return reinterpret_cast(e); @@ -342,9 +373,11 @@ void PADispatchMainLoop::defer_enable(pa_defer_event *e, int b) // std::cout << "PADispatchMainLoop::defer_enable(): with userdata=" << ev->userdata << ", enable=" << b << std::endl; - if (!ev->active && b) - dispatch_async_f(q, e, defer_event_fire); - ev->active = !!b; + ev->active=!!b; + if(ev->active && !ev->freed && !ev->scheduled) { + ev->scheduled=true; + dispatch_async_f(q,e,defer_event_fire); + } } void PADispatchMainLoop::defer_free(pa_defer_event *e) @@ -355,10 +388,11 @@ void PADispatchMainLoop::defer_free(pa_defer_event *e) // std::cout << "PADispatchMainLoop::defer_free(): with userdata=" << ev->userdata << std::endl; ev->active = false; - + ev->freed=true; dispatch_async(q, ^{ if (ev->destroy) ev->destroy(ev->loop->getAPI(), e, ev->userdata); + dispatch_group_leave(ev->loop->m_sources); delete ev; }); } diff --git a/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.h b/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.h index ff83d1e15..1e86f9e99 100644 --- a/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.h +++ b/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.h @@ -30,8 +30,12 @@ public: void suspend(); void resume(); + void sync(void (^block)(void)); + void async(void (^block)(void)); pa_mainloop_api* getAPI(); + // The serial queue all pa_* calls and callbacks run on. + dispatch_queue_t queue() const { return m_queue; } private: static pa_io_event* io_new(pa_mainloop_api *a, int fd, pa_io_event_flags_t events, pa_io_event_cb_t cb, void *userdata); static void io_enable(pa_io_event *e, pa_io_event_flags_t events); @@ -50,6 +54,7 @@ private: static void defer_event_fire(void* context); private: dispatch_queue_t m_queue; + dispatch_group_t m_sources; pa_mainloop_api m_api = {0}; bool m_suspended = true; }; diff --git a/src/CoreAudio/CoreAudioComponent/AUHAL.cpp b/src/CoreAudio/CoreAudioComponent/AUHAL.cpp index fe4d1eaa1..769b06586 100644 --- a/src/CoreAudio/CoreAudioComponent/AUHAL.cpp +++ b/src/CoreAudio/CoreAudioComponent/AUHAL.cpp @@ -1,3 +1,4 @@ +#include "../AudioTrace.h" /* This file is part of Darling. @@ -19,6 +20,15 @@ along with Darling. If not, see . #include "AUHAL.h" #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #pragma GCC visibility push(default) AUDIOCOMPONENT_ENTRY(AUOutputBaseFactory, AUHAL); @@ -29,8 +39,11 @@ enum { kInputBus }; +// Like macOS: element 0 is the output (app -> device) side, element 1 the input +// (device -> app) side, on both scopes. Without output element 1 the app's +// recording format and AudioUnitRender(bus 1) had nowhere to go. AUHAL::AUHAL(AudioComponentInstance inInstance, bool supportRecording) -: AUBase(inInstance, 1, supportRecording ? 1 : 0) +: AUBase(inInstance, supportRecording ? 2 : 1, supportRecording ? 2 : 1) { UInt32 propSize = sizeof(AudioDeviceID); AudioHardwareGetProperty(kAudioHardwarePropertyDefaultOutputDevice, &propSize, &m_outputDevice); @@ -42,6 +55,37 @@ AUHAL::~AUHAL() Stop(); } +UInt32 AUHAL::GetChannelLayoutTags(AudioUnitScope scope,AudioUnitElement element,AudioChannelLayoutTag* tags) { + if(element!=kOutputBus || (scope!=kAudioUnitScope_Input && scope!=kAudioUnitScope_Output)) return 0; + UInt32 channels=GetStreamFormat(scope,element).mChannelsPerFrame; + if(tags) *tags=channels==1?kAudioChannelLayoutTag_Mono:channels==2?kAudioChannelLayoutTag_Stereo:(kAudioChannelLayoutTag_DiscreteInOrder|channels); + return 1; +} + +UInt32 AUHAL::GetAudioChannelLayout(AudioUnitScope scope,AudioUnitElement element,AudioChannelLayout* layout,Boolean& writable) { + AudioChannelLayoutTag tag; + if(!GetChannelLayoutTags(scope,element,&tag)) return 0; + writable=!m_running; + if(layout) { layout->mChannelLayoutTag=tag;layout->mChannelBitmap=0;layout->mNumberChannelDescriptions=0; } + return offsetof(AudioChannelLayout,mChannelDescriptions); +} + +OSStatus AUHAL::SetAudioChannelLayout(AudioUnitScope scope,AudioUnitElement element,const AudioChannelLayout* layout) { + AudioChannelLayoutTag tag; + if(!layout || !GetChannelLayoutTags(scope,element,&tag)) return kAudioUnitErr_InvalidElement; + if(layout->mChannelLayoutTag==tag) return noErr; + /* darling-lol: a layout is acceptable when it describes the stream's number of channels, whichever way it is written: + * explicit channel descriptions (League sends two: left, right), a channel bitmap, or another tag with that count. + * Only the exact tag was accepted, so the game's stereo description list was refused (-10851) and it rebuilt its + * audio graph in a loop. */ + const UInt32 wanted=GetStreamFormat(scope,element).mChannelsPerFrame; + UInt32 channels; + if(layout->mChannelLayoutTag==kAudioChannelLayoutTag_UseChannelDescriptions) channels=layout->mNumberChannelDescriptions; + else if(layout->mChannelLayoutTag==kAudioChannelLayoutTag_UseChannelBitmap) channels=__builtin_popcount(layout->mChannelBitmap); + else channels=layout->mChannelLayoutTag&0xffff; + return channels==wanted?noErr:kAudioUnitErr_InvalidPropertyValue; +} + bool AUHAL::CanScheduleParameters() const { return true; @@ -49,7 +93,36 @@ bool AUHAL::CanScheduleParameters() const bool AUHAL::StreamFormatWritable(AudioUnitScope scope, AudioUnitElement element) { - return !m_running && IsInitialized(); + return !m_running; +} + +// AUBase only accepts the canonical layout (Float32, non-interleaved). A real AUHAL +// converts any linear PCM, and Vivox asks for interleaved Float32 on both client sides +// (recording: output scope element 1; playback: input scope element 0, at its own rate). +// Those accept what PulseAudio converts and our copies handle: native-endian float32 or +// signed 16/32-bit integers, either layout. Missing frames are padded with zeros, which is +// only silence for float and signed samples, so unsigned (and 8-bit) formats are left out; +// so is 24-bit (PulseAudio may use 4-byte samples while we copy 3). Device sides keep the +// canonical rule. +bool AUHAL::ValidFormat(AudioUnitScope scope, AudioUnitElement element, const CAStreamBasicDescription& format) +{ + const bool clientSide = (scope == kAudioUnitScope_Output && element == kInputBus) + || (scope == kAudioUnitScope_Input && element == kOutputBus); + if (!clientSide) + return AUBase::ValidFormat(scope, element, format); + + const UInt32 flags = format.mFormatFlags; + const UInt32 bits = format.mBitsPerChannel; + const bool isFloat = flags & kAudioFormatFlagIsFloat; + if (format.mFormatID != kAudioFormatLinearPCM || format.mFramesPerPacket != 1 + || format.mSampleRate <= 0 || format.mChannelsPerFrame < 1 || format.mChannelsPerFrame > 32 + || (flags & kAudioFormatFlagIsBigEndian) != (kAudioFormatFlagsNativeEndian & kAudioFormatFlagIsBigEndian)) + return false; + if (isFloat ? bits != 32 : (!(flags & kAudioFormatFlagIsSignedInteger) || (bits != 16 && bits != 32))) + return false; + + const UInt32 frameBytes = (flags & kAudioFormatFlagIsNonInterleaved) ? bits / 8 : format.mChannelsPerFrame * bits / 8; + return format.mBytesPerFrame == frameBytes && format.mBytesPerPacket == frameBytes; } OSStatus AUHAL::Version() @@ -57,45 +130,129 @@ OSStatus AUHAL::Version() return 1; } -// Provide data from the microphone -OSStatus AUHAL::Render(AudioUnitRenderActionFlags& ioActionFlags, const AudioTimeStamp& inTimeStamp, UInt32 inNumberFrames) +// Provide data from the microphone: AudioUnitRender(bus 1), normally called from the input callback. +// Never blocks; frames not recorded yet are silence. Bypasses NeedsToRender(), as a real AUHAL +// renders whatever timestamp the client passes. +OSStatus AUHAL::RenderBus(AudioUnitRenderActionFlags& ioActionFlags, const AudioTimeStamp& inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames) { - std::unique_lock lk(m_dataAvailableMutex); + if (inBusNumber != kInputBus || !m_enableInput) + return kAudioUnitErr_NoConnection; - m_dataAvailableCV.wait(lk, [=]{ return m_dataAvailable; }); + const CAStreamBasicDescription& desc = GetStreamFormat(kAudioUnitScope_Output, kInputBus); + const UInt32 sampleBytes = desc.mBitsPerChannel / 8; + const UInt32 frameBytes = desc.mChannelsPerFrame * sampleBytes; + if (!sampleBytes || !frameBytes) + return kAudioUnitErr_FormatNotSupported; + const bool planar = desc.mFormatFlags & kAudioFormatFlagIsNonInterleaved; AudioBufferList& abl = GetOutput(kInputBus)->GetBufferList(); + std::lock_guard lk(m_dataAvailableMutex); + const UInt32 frames = std::min(inNumberFrames, m_bufferUsed / frameBytes); + const uint8_t* source = m_buffer.get(); + + if (planar) + { + for (UInt32 c = 0; c < abl.mNumberBuffers && c < desc.mChannelsPerFrame; c++) + { + uint8_t* destination = static_cast(abl.mBuffers[c].mData); + for (UInt32 f = 0; f < frames; f++) + memcpy(destination + f * sampleBytes, source + f * frameBytes + c * sampleBytes, sampleBytes); + memset(destination + frames * sampleBytes, 0, (inNumberFrames - frames) * sampleBytes); + abl.mBuffers[c].mDataByteSize = inNumberFrames * sampleBytes; + } + } + else + { + uint8_t* destination = static_cast(abl.mBuffers[0].mData); + memcpy(destination, source, frames * frameBytes); + memset(destination + frames * frameBytes, 0, (inNumberFrames - frames) * frameBytes); + abl.mBuffers[0].mDataByteSize = inNumberFrames * frameBytes; + } - // TODO: Prepare for non-interleaved audio - UInt32 howMuch = std::min(abl.mBuffers[0].mDataByteSize, m_bufferUsed); - memcpy(abl.mBuffers[0].mData, m_buffer.get(), howMuch); - abl.mBuffers[0].mDataByteSize = howMuch; + // Level only (no samples): shows whether the client pulls real sound or silence. + static std::atomic renders{0}; + const unsigned n = renders++; + if (rclolAudioTraceEnabled() && (n < 40 || n % 1000 == 0)) + { + float peak = 0; + const UInt32 count = frames * desc.mChannelsPerFrame; + if (desc.IsFloat() && sampleBytes == 4) + for (UInt32 i = 0; i < count; i++) + peak = std::max(peak, std::abs(reinterpret_cast(source)[i])); + else if (sampleBytes == 2) + for (UInt32 i = 0; i < count; i++) + peak = std::max(peak, std::abs(reinterpret_cast(source)[i] / 32768.0f)); + rclolAudioTrace("AUDIO_INPUT_RENDER n=%u frames=%u available=%u planar=%d peak=%.4f\n", + n, inNumberFrames, frames, (int) planar, peak); + } return noErr; } OSStatus AUHAL::Start() { + if(rclolAudioTraceEnabled()) rclolAudioTrace("AUDIO_UNIT_NATIVE_START output=%d input=%d\n",m_enableOutput,m_enableInput); if (m_running) return noErr; if (m_enableOutput) { // std::cout << "Output is enabled, starting playback\n"; - AudioDeviceCreateIOProcID(m_outputDevice, playbackCallback, this, &m_outputProcID); + OSStatus status=AudioDeviceCreateIOProcID(m_outputDevice,playbackCallback,this,&m_outputProcID); + if(status) return status; // m_auhalData.open("/tmp/auhal.raw", std::ios_base::binary | std::ios_base::out); const CAStreamBasicDescription& desc = GetStreamFormat(kAudioUnitScope_Input, kOutputBus); - AudioDeviceSetProperty(m_outputDevice, nullptr, 0, false, kAudioDevicePropertyStreamFormat, sizeof(AudioStreamBasicDescription), &desc); - AudioDeviceStart(m_outputDevice, m_outputProcID); + AudioStreamBasicDescription hardware=desc; + hardware.mFormatFlags&=~kAudioFormatFlagIsNonInterleaved; + hardware.mBytesPerFrame=hardware.mChannelsPerFrame*hardware.mBitsPerChannel/8; + hardware.mBytesPerPacket=hardware.mBytesPerFrame; + status=AudioDeviceSetProperty(m_outputDevice,nullptr,0,false,kAudioDevicePropertyStreamFormat,sizeof(hardware),&hardware); + if(!status) status=AudioDeviceStart(m_outputDevice,m_outputProcID); + if(status) {AudioDeviceDestroyIOProcID(m_outputDevice,m_outputProcID);m_outputProcID=0;return status;} } if (m_enableInput) { - AudioDeviceCreateIOProcID(m_inputDevice, recordCallback, this, &m_inputProcID); - const CAStreamBasicDescription& desc = GetStreamFormat(kAudioUnitScope_Output, kInputBus); - AudioDeviceSetProperty(m_inputDevice, nullptr, 0, true, kAudioDevicePropertyStreamFormat, sizeof(AudioStreamBasicDescription), &desc); - AudioDeviceStart(m_inputDevice, m_inputProcID); + // The device records directly in the client's bus 1 format (PulseAudio converts rate, + // channels and sample type); only planar layouts are converted, in RenderBus(). + OSStatus status = AudioDeviceCreateIOProcID(m_inputDevice, recordCallback, this, &m_inputProcID); + if (!status) + { + const CAStreamBasicDescription& desc = GetStreamFormat(kAudioUnitScope_Output, kInputBus); + AudioStreamBasicDescription hardware = desc; + hardware.mFormatFlags &= ~kAudioFormatFlagIsNonInterleaved; + hardware.mBytesPerFrame = hardware.mChannelsPerFrame * hardware.mBitsPerChannel / 8; + hardware.mBytesPerPacket = hardware.mBytesPerFrame; + // The device keeps its buffer size in bytes; keep the frame count across the format change. + AudioObjectPropertyAddress p = {kAudioDevicePropertyBufferFrameSize, kAudioObjectPropertyScopeInput, kAudioObjectPropertyElementMaster}; + UInt32 frames = 0, size = sizeof(frames); + const bool haveFrames = AudioObjectGetPropertyData(m_inputDevice, &p, 0, nullptr, &size, &frames) == noErr; + status = AudioDeviceSetProperty(m_inputDevice, nullptr, 0, true, kAudioDevicePropertyStreamFormat, sizeof(hardware), &hardware); + if (!status && haveFrames) + AudioObjectSetPropertyData(m_inputDevice, &p, 0, nullptr, sizeof(frames), &frames); + { + std::lock_guard lk(m_dataAvailableMutex); + m_bufferUsed = 0; + } + if (!status) + status = AudioDeviceStart(m_inputDevice, m_inputProcID); + if (status) + { + AudioDeviceDestroyIOProcID(m_inputDevice, m_inputProcID); + m_inputProcID = 0; + } + } + if (status) + { + if (m_outputProcID) + { + AudioDeviceStop(m_outputDevice, m_outputProcID); + AudioDeviceDestroyIOProcID(m_outputDevice, m_outputProcID); + m_outputProcID = 0; + } + return status; + } } m_running = m_enableOutput || m_enableInput; @@ -111,20 +268,49 @@ OSStatus AUHAL::Stop() { AudioDeviceStop(m_outputDevice, m_outputProcID); AudioDeviceDestroyIOProcID(m_outputDevice, m_outputProcID); + m_outputProcID=0; } if (m_inputProcID) { AudioDeviceStop(m_inputDevice, m_inputProcID); AudioDeviceDestroyIOProcID(m_inputDevice, m_inputProcID); + m_inputProcID=0; } - + m_running=false; return noErr; } +// Volume/mute/gain related property selectors (HAL device properties and AUHAL parameters). +static bool rclolVolumeRelated(UInt32 id) +{ + switch (id) + { + case 'volm': case 'vold': case 'vdb#': case 'mute': case 'thru': + case 'pvol': case 'pvod': case 'mvol': case 'mbal': case 'span': + case 'svlm': case 'smut': case 'v2db': case 'db2v': + return true; + } + return false; +} + OSStatus AUHAL::SetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, const void* inData, UInt32 inDataSize) { + if (rclolAudioTraceEnabled() && rclolVolumeRelated(inID)) + rclolAudioTrace("AUHAL_SET_VOLPROP property=%08x scope=%u element=%u size=%u value=%g\n",inID,inScope,inElement,inDataSize, + (inData && inDataSize==sizeof(Float32))?(double)*(const Float32*)inData:0.0); switch (inID) { + case kAudioDevicePropertyBufferFrameSize: { + // An input-only unit (EnableIO output=0) sizes its input device's IO cycle. + AudioObjectPropertyAddress p={inID,kAudioObjectPropertyScopeOutput,kAudioObjectPropertyElementMaster}; + OSStatus status=noErr; + if(m_enableOutput || !m_enableInput) status=AudioObjectSetPropertyData(m_outputDevice,&p,0,nullptr,inDataSize,inData); + if(!status && m_enableInput) { + p.mScope=kAudioObjectPropertyScopeInput; + status=AudioObjectSetPropertyData(m_inputDevice,&p,0,nullptr,inDataSize,inData); + } + return status; + } case kAudioOutputUnitProperty_SetInputCallback: { ca_require(inDataSize == sizeof(AURenderCallbackStruct), InvalidPropertyValue); @@ -161,7 +347,14 @@ OSStatus AUHAL::SetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au ca_require(inDataSize == sizeof(AudioDeviceID), InvalidPropertyValue); const AudioDeviceID* dev = static_cast(inData); - if (inElement == kOutputBus) + // macOS AUHAL has one current device, normally set on element 0; for an + // input-only unit (EnableIO input=1, output=0) that is the capture device. + if (inElement == kOutputBus && m_enableInput && !m_enableOutput) + { + m_inputDevice = *dev; + PropertyChanged(inID, inScope, inElement); + } + else if (inElement == kOutputBus) { m_outputDevice = *dev; PropertyChanged(inID, inScope, inElement); @@ -176,20 +369,28 @@ OSStatus AUHAL::SetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au return noErr; } } - return AUBase::SetProperty(inID, inScope, inElement, inData, inDataSize); + { + OSStatus status=AUBase::SetProperty(inID,inScope,inElement,inData,inDataSize); + if(status && rclolAudioTraceEnabled()) rclolAudioTrace("AUDIO_UNIT_SET property=%u scope=%u element=%u status=%d\n",inID,inScope,inElement,status); + return status; + } InvalidPropertyValue: return kAudioUnitErr_InvalidPropertyValue; } OSStatus AUHAL::GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, UInt32& outDataSize, Boolean& outWritable) { + if (rclolAudioTraceEnabled() && rclolVolumeRelated(inID)) + rclolAudioTrace("AUHAL_INFO_VOLPROP property=%08x scope=%u element=%u\n",inID,inScope,inElement); switch (inID) { + case kAudioDevicePropertyBufferFrameSize: + outDataSize=sizeof(UInt32);outWritable=true;return noErr; case kAudioOutputUnitProperty_SetInputCallback: { outDataSize = sizeof(AURenderCallbackStruct); outWritable = true; - break; + return noErr; } case kAudioOutputUnitProperty_EnableIO: { @@ -210,13 +411,23 @@ OSStatus AUHAL::GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope return noErr; } } - return AUBase::GetPropertyInfo(inID, inScope, inElement, outDataSize, outWritable); + OSStatus status=AUBase::GetPropertyInfo(inID,inScope,inElement,outDataSize,outWritable); + if(status && rclolAudioTraceEnabled()) rclolAudioTrace("AUDIO_UNIT_INFO property=%u scope=%u element=%u status=%d\n",inID,inScope,inElement,status); + return status; } OSStatus AUHAL::GetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, void* outData) { + if (rclolAudioTraceEnabled() && rclolVolumeRelated(inID)) + rclolAudioTrace("AUHAL_GET_VOLPROP property=%08x scope=%u element=%u\n",inID,inScope,inElement); switch (inID) { + case kAudioDevicePropertyBufferFrameSize: { + const bool input=m_enableInput && !m_enableOutput; + AudioObjectPropertyAddress p={inID,input?kAudioObjectPropertyScopeInput:kAudioObjectPropertyScopeOutput,kAudioObjectPropertyElementMaster}; + UInt32 size=sizeof(UInt32); + return AudioObjectGetPropertyData(input?m_inputDevice:m_outputDevice,&p,0,nullptr,&size,outData); + } case kAudioOutputUnitProperty_SetInputCallback: { memcpy(outData, &m_outputAvailableCb, sizeof(m_outputAvailableCb)); @@ -228,12 +439,12 @@ OSStatus AUHAL::GetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au if (inElement == kOutputBus) { UInt32 value = m_enableOutput; - memcpy(&outData, &value, sizeof(value)); + memcpy(outData, &value, sizeof(value)); } else if (inElement == kInputBus) { UInt32 value = m_enableInput; - memcpy(&outData, &value, sizeof(value)); + memcpy(outData, &value, sizeof(value)); } else return kAudioUnitErr_InvalidElement; @@ -241,7 +452,11 @@ OSStatus AUHAL::GetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au } case kAudioOutputUnitProperty_CurrentDevice: { - if (inElement == kOutputBus) + if (inElement == kOutputBus && m_enableInput && !m_enableOutput) + { + memcpy(outData, &m_inputDevice, sizeof(m_inputDevice)); + } + else if (inElement == kOutputBus) { memcpy(outData, &m_outputDevice, sizeof(m_outputDevice)); } @@ -277,9 +492,89 @@ OSStatus AUHAL::recordCallback(AudioObjectID inObjectID, return This->doRecord(inNow, inInputData, inInputTime); } +// Diagnostic only: classify each rendered block (see AUDIO_BLOCK_STATE). +void AUHAL::traceBlock(const AudioTimeStamp* inNow, const AudioTimeStamp* inOutputTime, OSStatus result, + const CAStreamBasicDescription& desc, UInt32 nFrames, AudioBufferList* out) +{ + static std::atomic stateLines(0); + static mach_timebase_info_data_t tb={0,0}; + if(!tb.denom) mach_timebase_info(&tb); + BlockTrace& t=m_blockTrace; + const unsigned long long cb=++t.callbacks; + if(result) { + if(++t.pullFailures<=50 || t.pullFailures%200==0) + rclolAudioTrace("AUDIO_PULL_FAIL unit=%p cb=%llu status=%d frames=%u count=%llu\n", + (void*)this,cb,(int)result,nFrames,t.pullFailures); + return; + } + if(!desc.IsFloat() || desc.mBitsPerChannel!=32 || !nFrames || !desc.mChannelsPerFrame) return; + const bool planar=desc.mFormatFlags&kAudioFormatFlagIsNonInterleaved; + const UInt32 ch=desc.mChannelsPerFrame; + const float* base=planar?(const float*)m_playbackSamples.data():(const float*)out->mBuffers[0].mData; + if(!base) return; + auto at=[&](UInt32 c,UInt32 f)->float { return planar?base[(size_t)c*nFrames+f]:base[(size_t)f*ch+c]; }; + float peak[2]={0,0}; double sq[2]={0,0}; bool halves[2]={true,true}; bool zero=true; + const UInt32 half=nFrames/2; + for(UInt32 c=0;c1?sqrt(sq[1]/nFrames):0; + bool same=false; + const size_t total=(size_t)nFrames*ch; + if(t.havePrev && t.prev.size()==total) { + same=true; + for(UInt32 c=0;cmSampleTime; + if(t.haveTime) { + dSample=sample-t.lastSample; + dHostMs=(double)(inOutputTime->mHostTime-t.lastHost)*tb.numer/tb.denom/1e6; + if(dHostMs>t.maxGapMs) t.maxGapMs=dHostMs; + } + t.haveTime=true; t.lastSample=sample; t.lastHost=inOutputTime->mHostTime; + const bool transition=state!=t.state; + if(transition && t.state>=0 && stateLines.fetch_add(1)<2000) + rclolAudioTrace("AUDIO_BLOCK_STATE unit=%p from=%s to=%s cb=%llu frames=%u peakL=%g peakR=%g rmsL=%g rmsR=%g\n", + (void*)this,names[t.state],names[state],cb,nFrames,peak[0],peak[1],rms0,rms1); + if(cb<=40 || cb%200==0 || transition) + rclolAudioTrace("AUDIO_BLOCK unit=%p cb=%llu state=%s frames=%u peakL=%g peakR=%g rmsL=%g rmsR=%g " + "halvesL=%d halvesR=%d same=%d zero=%d outSample=%.0f nowSample=%.0f dSample=%.0f outHost=%llu nowHost=%llu " + "dHostMs=%.3f maxGapMs=%.3f\n", + (void*)this,cb,names[state],nFrames,peak[0],peak[1],rms0,rms1,(int)halves[0],(int)halves[1],(int)same,(int)zero, + sample,inNow->mSampleTime,dSample,(unsigned long long)inOutputTime->mHostTime, + (unsigned long long)inNow->mHostTime,dHostMs,t.maxGapMs); + if(cb%2000==0) + rclolAudioTrace("AUDIO_BLOCK_SUMMARY unit=%p cb=%llu silent=%llu live=%llu repeat=%llu halves=%llu pullFail=%llu maxGapMs=%.3f\n", + (void*)this,cb,t.stateCounts[0],t.stateCounts[1],t.stateCounts[2],t.stateCounts[3],t.pullFailures,t.maxGapMs); + t.state=state; +} + OSStatus AUHAL::doPlayback(const AudioTimeStamp* inNow, AudioBufferList* outOutputData, const AudioTimeStamp* inOutputTime) { // std::cout << "AUHAL::DoPlayback()\n"; + static unsigned stackTraces=0; + if(rclolAudioTraceEnabled() && stackTraces++<2) + rclolAudioTrace("AUDIO_CALLBACK_STACK bytes=%zu\n",pthread_get_stacksize_np(pthread_self())); if (!HasInput(0)) { // std::cerr << "No connection\n"; @@ -290,9 +585,75 @@ OSStatus AUHAL::doPlayback(const AudioTimeStamp* inNow, AudioBufferList* outOutp AudioUnitRenderActionFlags flags = kAudioUnitRenderAction_PreRender; const CAStreamBasicDescription& desc = GetStreamFormat(kAudioUnitScope_Input, kOutputBus); - UInt32 nFrames = outOutputData->mBuffers[0].mDataByteSize / (desc.mBytesPerFrame / outOutputData->mBuffers[0].mNumberChannels); - result = GetInput(kOutputBus)->PullInputWithBufferList(flags, *inNow, kOutputBus, nFrames, outOutputData); - + if(outOutputData->mNumberBuffers!=1 || !desc.mChannelsPerFrame || + !desc.mBitsPerChannel || desc.mBitsPerChannel%8) return kAudioUnitErr_FormatNotSupported; + UInt32 sampleBytes=desc.mBitsPerChannel/8; + UInt32 frameBytes=desc.mChannelsPerFrame*sampleBytes; + UInt32 nFrames=outOutputData->mBuffers[0].mDataByteSize/frameBytes; + if(desc.mFormatFlags&kAudioFormatFlagIsNonInterleaved) { + m_playbackSamples.resize(nFrames*frameBytes); + // Preserve fresh-buffer silence for callbacks that leave samples unwritten. + std::fill(m_playbackSamples.begin(),m_playbackSamples.end(),0); + m_playbackBufferList.resize(offsetof(AudioBufferList,mBuffers)+desc.mChannelsPerFrame*sizeof(AudioBuffer)); + AudioBufferList* planar=(AudioBufferList*)m_playbackBufferList.data(); + planar->mNumberBuffers=desc.mChannelsPerFrame; + for(UInt32 c=0;cmBuffers[c].mNumberChannels=1; + planar->mBuffers[c].mDataByteSize=nFrames*sampleBytes; + planar->mBuffers[c].mData=m_playbackSamples.data()+c*nFrames*sampleBytes; + } + result=GetInput(kOutputBus)->PullInputWithBufferList(flags,*inNow,kOutputBus,nFrames,planar); + if(!result) { + auto* destination=(unsigned char*)outOutputData->mBuffers[0].mData; + for(UInt32 f=0;fmBuffers[c].mData+f*sampleBytes,sampleBytes); + } + } else { + result=GetInput(kOutputBus)->PullInputWithBufferList(flags,*inNow,kOutputBus,nFrames,outOutputData); + } + + if(rclolAudioTraceEnabled()) traceBlock(inNow,inOutputTime,result,desc,nFrames,outOutputData); + + // Diagnostic: Chromium's audio source can start handing out one identical + // block forever (heard as static). Log the timestamp Darling supplied at the + // moment that begins, so we can tell whether our clock inputs look sane. + { + const unsigned char* produced; + size_t producedBytes; + if(desc.mFormatFlags&kAudioFormatFlagIsNonInterleaved) { + produced=m_playbackSamples.data(); + producedBytes=(size_t)nFrames*frameBytes; + } else { + produced=(const unsigned char*)outOutputData->mBuffers[0].mData; + producedBytes=outOutputData->mBuffers[0].mDataByteSize; + } + uint64_t sum=1469598103934665603ull; + for(size_t i=0;imHostTime,inOutputTime->mSampleTime, + (unsigned)inOutputTime->mFlags); + } else { + repeatRun=0; + } + lastSum=sum; + } + + static unsigned traceRenders=0; + if (rclolAudioTraceEnabled() && traceRenders++ < 16) { + float peak=0; + if (!result && desc.IsFloat() && sampleBytes==4) { + const float* samples=static_cast(outOutputData->mBuffers[0].mData); + for(UInt32 i=0;imBuffers[0].mDataByteSize/4;++i) + peak=std::max(peak,std::abs(samples[i])); + } + rclolAudioTrace("AUDIO_RENDER status=%d flags=%u frames=%u peak=%g\n",result,flags,nFrames,peak); + } // std::cout << "Pull result: " << result << std::endl; // std::cout << "Bytes: " << outOutputData->mBuffers[0].mDataByteSize << std::endl; // m_auhalData.write((char*) outOutputData->mBuffers[0].mData, outOutputData->mBuffers[0].mDataByteSize); @@ -303,30 +664,46 @@ OSStatus AUHAL::doPlayback(const AudioTimeStamp* inNow, AudioBufferList* outOutp OSStatus AUHAL::doRecord(const AudioTimeStamp* inNow, const AudioBufferList* inInputData, const AudioTimeStamp* inInputTime) { - // TODO: Prepare for non-interleaved audio - std::unique_lock lk(m_dataAvailableMutex); - - if (m_bufferSize < inInputData->mBuffers[0].mDataByteSize) - { - m_buffer.reset(new uint8_t[inInputData->mBuffers[0].mDataByteSize]); - m_bufferSize = inInputData->mBuffers[0].mDataByteSize; - } - - m_bufferUsed = inInputData->mBuffers[0].mDataByteSize; - memcpy(m_buffer.get(), inInputData->mBuffers[0].mData, m_bufferUsed); - - m_dataAvailable = true; + // The device records interleaved data in the bus 1 output format (see Start()). + const CAStreamBasicDescription& desc = GetStreamFormat(kAudioUnitScope_Output, kInputBus); + const UInt32 frameBytes = desc.mChannelsPerFrame * (desc.mBitsPerChannel / 8); + if (!inInputData || !inInputData->mNumberBuffers || !frameBytes) + return noErr; - lk.unlock(); - m_dataAvailableCV.notify_one(); + const uint8_t* source = static_cast(inInputData->mBuffers[0].mData); + const UInt32 totalFrames = inInputData->mBuffers[0].mDataByteSize / frameBytes; + // AudioUnitRender refuses more than kAudioUnitProperty_MaximumFramesPerSlice frames. + const UInt32 maxFrames = std::max(1, GetMaxFramesPerSlice()); + static mach_timebase_info_data_t tb = {0, 0}; + if (!tb.denom) mach_timebase_info(&tb); - if (m_outputAvailableCb.inputProc) + for (UInt32 offset = 0; offset < totalFrames; ) { - AudioUnitRenderActionFlags flags = kAudioUnitRenderAction_PostRender; - const CAStreamBasicDescription& desc = GetStreamFormat(kAudioUnitScope_Output, kInputBus); - UInt32 numFrames = m_bufferUsed / desc.mBytesPerFrame; + const UInt32 numFrames = std::min(maxFrames, totalFrames - offset); + const size_t bytes = size_t(numFrames) * frameBytes; + { + std::lock_guard lk(m_dataAvailableMutex); + if (m_bufferSize < bytes) + { + m_buffer.reset(new uint8_t[bytes]); + m_bufferSize = bytes; + } + memcpy(m_buffer.get(), source + size_t(offset) * frameBytes, bytes); + m_bufferUsed = bytes; + } - m_outputAvailableCb.inputProc(m_outputAvailableCb.inputProcRefCon, &flags, inInputTime, kInputBus, numFrames, nullptr); + AudioTimeStamp time = *inInputTime; + time.mSampleTime += offset; + if (offset && desc.mSampleRate > 0 && tb.numer) + time.mHostTime += uint64_t(double(offset) / desc.mSampleRate * 1e9 * tb.denom / tb.numer); + + // The client pulls the slice with AudioUnitRender(bus 1, numFrames) from this callback. + if (m_outputAvailableCb.inputProc) + { + AudioUnitRenderActionFlags flags = 0; + m_outputAvailableCb.inputProc(m_outputAvailableCb.inputProcRefCon, &flags, &time, kInputBus, numFrames, nullptr); + } + offset += numFrames; } return noErr; diff --git a/src/CoreAudio/CoreAudioComponent/AUHAL.h b/src/CoreAudio/CoreAudioComponent/AUHAL.h index bc21bcbbb..fad727962 100644 --- a/src/CoreAudio/CoreAudioComponent/AUHAL.h +++ b/src/CoreAudio/CoreAudioComponent/AUHAL.h @@ -25,6 +25,7 @@ along with Darling. If not, see . #include #include #include +#include class AUHAL : public AUBase { @@ -32,8 +33,12 @@ public: AUHAL(AudioComponentInstance inInstance, bool supportRecording = true); ~AUHAL(); + UInt32 GetChannelLayoutTags(AudioUnitScope scope,AudioUnitElement element,AudioChannelLayoutTag* tags) override; + UInt32 GetAudioChannelLayout(AudioUnitScope scope,AudioUnitElement element,AudioChannelLayout* layout,Boolean& writable) override; + OSStatus SetAudioChannelLayout(AudioUnitScope scope,AudioUnitElement element,const AudioChannelLayout* layout) override; bool CanScheduleParameters() const override; bool StreamFormatWritable(AudioUnitScope scope, AudioUnitElement element) override; + bool ValidFormat(AudioUnitScope scope, AudioUnitElement element, const CAStreamBasicDescription& format) override; OSStatus Version() override; OSStatus Start() override; @@ -43,7 +48,7 @@ public: OSStatus GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, UInt32& outDataSize, Boolean& outWritable) override; OSStatus GetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, void* outData) override; - OSStatus Render(AudioUnitRenderActionFlags& ioActionFlags, const AudioTimeStamp& inTimeStamp, UInt32 inNumberFrames) override; + OSStatus RenderBus(AudioUnitRenderActionFlags& ioActionFlags, const AudioTimeStamp& inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames) override; static OSStatus ComponentEntryDispatch(ComponentParameters *params, AUHAL *This); private: @@ -59,6 +64,7 @@ private: void* inClientData); OSStatus doPlayback(const AudioTimeStamp* inNow, AudioBufferList* outOutputData, const AudioTimeStamp* inOutputTime); + void traceBlock(const AudioTimeStamp* inNow, const AudioTimeStamp* inOutputTime, OSStatus result, const CAStreamBasicDescription& desc, UInt32 nFrames, AudioBufferList* out); OSStatus doRecord(const AudioTimeStamp* inNow, const AudioBufferList* inInputData, const AudioTimeStamp* inInputTime); protected: @@ -69,13 +75,29 @@ protected: AudioDeviceID m_outputDevice, m_inputDevice; AudioDeviceIOProcID m_outputProcID = 0, m_inputProcID = 0; - bool m_dataAvailable = false; - std::condition_variable m_dataAvailableCV; + // The latest recorded slice (interleaved, in the bus 1 output format), handed to + // AudioUnitRender(bus 1) from inside the input callback. std::mutex m_dataAvailableMutex; - - // TODO: Prepare for non-interleaved audio std::unique_ptr m_buffer; size_t m_bufferSize = 0, m_bufferUsed = 0; + + // Playback callbacks run serially on the device's PulseAudio queue. + // Keep planar conversion storage across callbacks and start/stop cycles. + std::vector m_playbackSamples; + std::vector m_playbackBufferList; + + // Diagnostic-only block statistics (touched only when audio tracing is on). + struct BlockTrace { + unsigned long long callbacks = 0, pullFailures = 0; + unsigned long long stateCounts[4] = {0,0,0,0}; + int state = -1; + std::vector prev; + bool havePrev = false; + bool haveTime = false; + double lastSample = 0; + unsigned long long lastHost = 0; + double maxGapMs = 0; + } m_blockTrace; }; #endif diff --git a/src/CoreAudio/CoreAudioComponent/CMakeLists.txt b/src/CoreAudio/CoreAudioComponent/CMakeLists.txt index 38584e263..a4cf87529 100644 --- a/src/CoreAudio/CoreAudioComponent/CMakeLists.txt +++ b/src/CoreAudio/CoreAudioComponent/CMakeLists.txt @@ -29,6 +29,7 @@ set(component_sources PublicUtility/CAVectorUnit.cpp AUHAL.cpp SystemOutputAU.cpp + MatrixMixerAU.cpp DefaultOutputAU.cpp ) diff --git a/src/CoreAudio/CoreAudioComponent/DefaultOutputAU.cpp b/src/CoreAudio/CoreAudioComponent/DefaultOutputAU.cpp index 8b92b6c50..18a57e359 100644 --- a/src/CoreAudio/CoreAudioComponent/DefaultOutputAU.cpp +++ b/src/CoreAudio/CoreAudioComponent/DefaultOutputAU.cpp @@ -18,6 +18,8 @@ along with Darling. If not, see . */ #include "DefaultOutputAU.h" +#include +#include #pragma GCC visibility push(default) AUDIOCOMPONENT_ENTRY(AUOutputBaseFactory, DefaultOutputAU); #pragma GCC visibility pop @@ -32,11 +34,27 @@ DefaultOutputAU::DefaultOutputAU(AudioComponentInstance inInstance) { } +/* darling-lol: League's game process sets the default device as the current device of its DefaultOutput unit. Refusing that + * (kAudioUnitErr_InvalidProperty) made it throw its audio graph away and build a new one in a loop, so it never played + * anything. Accepting it harmless: the unit plays on the default device anyway. + * + * Only the game process gets this. With the property accepted everywhere, the League client's integrity module went to its + * error state (state 5, status -101) on every launch (2 of 2, 2026-10-11) and the client could not start a match; + * with the old behaviour it connected again (state 0). */ +static bool callerIsLeagueGame() +{ + static const bool game = []{ + const char* name = getprogname(); + return name && strcmp(name, "LeagueofLegends") == 0; + }(); + return game; +} + OSStatus DefaultOutputAU::GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, UInt32& outDataSize, Boolean& outWritable) { OSStatus status = AUHAL::GetPropertyInfo(inID, inScope, inElement, outDataSize, outWritable); - if (inID == kAudioOutputUnitProperty_CurrentDevice) + if (inID == kAudioOutputUnitProperty_CurrentDevice && !callerIsLeagueGame()) outWritable = false; return status; @@ -48,7 +66,9 @@ OSStatus DefaultOutputAU::SetProperty(AudioUnitPropertyID inID, AudioUnitScope i switch (inID) { case kAudioOutputUnitProperty_CurrentDevice: - return kAudioUnitErr_InvalidProperty; + if (!callerIsLeagueGame()) + return kAudioUnitErr_InvalidProperty; + break; } return AUHAL::SetProperty(inID, inScope, inElement, inData, inDataSize); } diff --git a/src/CoreAudio/CoreAudioComponent/Info.plist b/src/CoreAudio/CoreAudioComponent/Info.plist index da44a9f18..f658118d8 100644 --- a/src/CoreAudio/CoreAudioComponent/Info.plist +++ b/src/CoreAudio/CoreAudioComponent/Info.plist @@ -70,6 +70,24 @@ version 1 + + description + Float PCM matrix mixer + factoryFunction + MatrixMixerAUFactory + manufacturer + appl + name + Darling: MatrixMixer + sandboxSafe + + subtype + mxmx + type + aumx + version + 1 + CFBundleExecutable CoreAudio diff --git a/src/CoreAudio/include/CoreAudio/AudioHardwareBase.h b/src/CoreAudio/include/CoreAudio/AudioHardwareBase.h index 102614203..9b80671a6 100644 --- a/src/CoreAudio/include/CoreAudio/AudioHardwareBase.h +++ b/src/CoreAudio/include/CoreAudio/AudioHardwareBase.h @@ -13,12 +13,12 @@ typedef UInt32 AudioObjectPropertySelector; typedef UInt32 AudioObjectPropertyScope; typedef UInt32 AudioObjectPropertyElement; -struct AudioObjectPropertyAddress +typedef struct AudioObjectPropertyAddress { AudioObjectPropertySelector mSelector; AudioObjectPropertyScope mScope; AudioObjectPropertyElement mElement; -}; +} AudioObjectPropertyAddress; enum { @@ -87,6 +87,14 @@ enum }; +typedef AudioObjectID AudioStreamID; +enum { + kAudioStreamPropertyDirection = 'sdir', + kAudioStreamPropertyTerminalType = 'term', + kAudioStreamPropertyVirtualFormat = 'sfmt', + kAudioStreamPropertyPhysicalFormat = 'pft ', +}; + // TODO: A lot more is missing #ifdef __cplusplus @@ -94,4 +102,3 @@ enum #endif #endif /* AUDIOHARDWAREBASE_H */ - diff --git a/src/frameworks/AVFoundation/include/AVFoundation/AVCaptureDevice.h b/src/frameworks/AVFoundation/include/AVFoundation/AVCaptureDevice.h index cb2896957..b2c25fbd4 100644 --- a/src/frameworks/AVFoundation/include/AVFoundation/AVCaptureDevice.h +++ b/src/frameworks/AVFoundation/include/AVFoundation/AVCaptureDevice.h @@ -18,7 +18,22 @@ */ #include +#include + +typedef NS_ENUM(NSInteger, AVAuthorizationStatus) { + AVAuthorizationStatusNotDetermined = 0, + AVAuthorizationStatusRestricted = 1, + AVAuthorizationStatusDenied = 2, + AVAuthorizationStatusAuthorized = 3, +}; @interface AVCaptureDevice : NSObject ++ (NSArray *)devices; ++ (NSArray *)devicesWithMediaType:(AVMediaType)mediaType; ++ (AVCaptureDevice *)defaultDeviceWithMediaType:(AVMediaType)mediaType; + ++ (AVAuthorizationStatus)authorizationStatusForMediaType:(AVMediaType)mediaType; ++ (void)requestAccessForMediaType:(AVMediaType)mediaType completionHandler:(void (^)(BOOL granted))handler; + @end diff --git a/src/frameworks/AVFoundation/src/AVCaptureDevice.m b/src/frameworks/AVFoundation/src/AVCaptureDevice.m index cab525b91..4b3dd6541 100644 --- a/src/frameworks/AVFoundation/src/AVCaptureDevice.m +++ b/src/frameworks/AVFoundation/src/AVCaptureDevice.m @@ -18,9 +18,75 @@ */ #import +#include +#include +#include @implementation AVCaptureDevice ++ (NSArray *)devices { + return [NSArray array]; +} + ++ (NSArray *)devicesWithMediaType:(AVMediaType)mediaType { + (void)mediaType; + return [self devices]; +} + ++ (AVCaptureDevice *)defaultDeviceWithMediaType:(AVMediaType)mediaType { + (void)mediaType; + return nil; +} + + +// Darling has no TCC consent store. Microphone input works through CoreAudio +// (PulseAudio), so audio is authorized whenever CoreAudio reports a default input +// device. CoreAudio is looked up at run time: AVFoundation does not link it, and +// this query only reads its device table (no PulseAudio connection is made). +static BOOL hasDefaultInputDevice(void) +{ + static OSStatus (*getProperty)(AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32*, void*); + static dispatch_once_t once; + dispatch_once(&once, ^{ + // Never closed: CoreAudio keeps process-wide state once loaded. + void* coreAudio = dlopen("/System/Library/Frameworks/CoreAudio.framework/CoreAudio", RTLD_LAZY | RTLD_LOCAL); + if (coreAudio) + getProperty = dlsym(coreAudio, "AudioObjectGetPropertyData"); + }); + if (!getProperty) + return NO; + AudioDeviceID device = kAudioObjectUnknown; + UInt32 size = sizeof(device); + AudioObjectPropertyAddress address = { + kAudioHardwarePropertyDefaultInputDevice, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster + }; + return getProperty(kAudioObjectSystemObject, &address, 0, NULL, &size, &device) == noErr + && device != kAudioObjectUnknown; +} + ++ (AVAuthorizationStatus)authorizationStatusForMediaType:(AVMediaType)mediaType +{ + // There is no camera capture; report Denied so callers take their + // no-access path instead of raising an unrecognized-selector exception. + if ([mediaType isEqualToString: AVMediaTypeAudio] && hasDefaultInputDevice()) + return AVAuthorizationStatusAuthorized; + return AVAuthorizationStatusDenied; +} + ++ (void)requestAccessForMediaType:(AVMediaType)mediaType completionHandler:(void (^)(BOOL))handler +{ + if (!handler) + return; + BOOL granted = [self authorizationStatusForMediaType: mediaType] == AVAuthorizationStatusAuthorized; + // macOS calls the handler later on an arbitrary queue, never inside this call; + // callers may hold locks here or wait on the main thread for the result. + void (^copied)(BOOL) = [handler copy]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + copied(granted); + [copied release]; + }); +} + - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { return [NSMethodSignature signatureWithObjCTypes: "v@:"]; } diff --git a/src/frameworks/Carbon/HIToolbox/CMakeLists.txt b/src/frameworks/Carbon/HIToolbox/CMakeLists.txt index 3f0820484..7d310f61c 100644 --- a/src/frameworks/Carbon/HIToolbox/CMakeLists.txt +++ b/src/frameworks/Carbon/HIToolbox/CMakeLists.txt @@ -19,6 +19,7 @@ add_framework(HIToolbox SOURCES src/Keyboards.cpp + src/SecureInput.c src/Events.cpp src/CarbonEventsCore.c src/CarbonEvents.c diff --git a/src/frameworks/Carbon/HIToolbox/include/HIToolbox/Events.h b/src/frameworks/Carbon/HIToolbox/include/HIToolbox/Events.h index 8c37ce2a6..71ed125a5 100644 --- a/src/frameworks/Carbon/HIToolbox/include/HIToolbox/Events.h +++ b/src/frameworks/Carbon/HIToolbox/include/HIToolbox/Events.h @@ -19,6 +19,7 @@ OSStatus SetEventParameter(EventRef a, EventParamName b, EventParamType c, UInt3 extern UInt8 LMGetKbdType(void); +extern UInt8 LMGetKbdLast(void); enum { kVK_ANSI_A = 0x00, diff --git a/src/frameworks/Carbon/HIToolbox/include/HIToolbox/TextInputSources.h b/src/frameworks/Carbon/HIToolbox/include/HIToolbox/TextInputSources.h index 8d457c216..4b42c9af8 100644 --- a/src/frameworks/Carbon/HIToolbox/include/HIToolbox/TextInputSources.h +++ b/src/frameworks/Carbon/HIToolbox/include/HIToolbox/TextInputSources.h @@ -1,6 +1,7 @@ #ifndef _Carbon_TextInputSources_H_ #define _Carbon_TextInputSources_H_ #include +#include #ifdef __cplusplus extern "C" { @@ -25,6 +26,7 @@ extern TISInputSourceRef TISCopyCurrentKeyboardInputSource(void); extern void* TISGetInputSourceProperty(TISInputSourceRef inputSourceRef, CFStringRef key); extern TISInputSourceRef TISCopyCurrentASCIICapableKeyboardLayoutInputSource(void); +extern CFArrayRef TISCreateASCIICapableInputSourceList(void); #ifdef __cplusplus } diff --git a/src/frameworks/Carbon/HIToolbox/src/Events.cpp b/src/frameworks/Carbon/HIToolbox/src/Events.cpp index a37fc641f..98e29c97a 100644 --- a/src/frameworks/Carbon/HIToolbox/src/Events.cpp +++ b/src/frameworks/Carbon/HIToolbox/src/Events.cpp @@ -4,3 +4,10 @@ UInt8 LMGetKbdType(void) { return 0; } + +UInt8 LMGetKbdLast(void) +{ + // Darling exposes one logical keyboard through its current XKB layout. + // Its last-used keyboard type is therefore the current keyboard type. + return LMGetKbdType(); +} diff --git a/src/frameworks/Carbon/HIToolbox/src/TextInputSources.mm b/src/frameworks/Carbon/HIToolbox/src/TextInputSources.mm index 347cb530c..f73278a35 100644 --- a/src/frameworks/Carbon/HIToolbox/src/TextInputSources.mm +++ b/src/frameworks/Carbon/HIToolbox/src/TextInputSources.mm @@ -85,3 +85,10 @@ TISInputSourceRef TISCopyCurrentASCIICapableKeyboardLayoutInputSource(void) return NULL; } + +CFArrayRef TISCreateASCIICapableInputSourceList(void) +{ + /* Until ASCII-capable layouts can be enumerated, return an owned empty + * list. A NULL list would crash callers that unconditionally CFRelease. */ + return CFArrayCreate(NULL, NULL, 0, &kCFTypeArrayCallBacks); +} diff --git a/src/frameworks/CoreLocation/include/CoreLocation/CLLocation.h b/src/frameworks/CoreLocation/include/CoreLocation/CLLocation.h index 090de1df2..fe1f95f3b 100644 --- a/src/frameworks/CoreLocation/include/CoreLocation/CLLocation.h +++ b/src/frameworks/CoreLocation/include/CoreLocation/CLLocation.h @@ -19,6 +19,15 @@ #include +typedef double CLLocationAccuracy; + +extern const CLLocationAccuracy kCLLocationAccuracyBestForNavigation; +extern const CLLocationAccuracy kCLLocationAccuracyBest; +extern const CLLocationAccuracy kCLLocationAccuracyNearestTenMeters; +extern const CLLocationAccuracy kCLLocationAccuracyHundredMeters; +extern const CLLocationAccuracy kCLLocationAccuracyKilometer; +extern const CLLocationAccuracy kCLLocationAccuracyThreeKilometers; + @interface CLLocation : NSObject @end diff --git a/src/frameworks/CoreLocation/include/CoreLocation/CLLocationManager.h b/src/frameworks/CoreLocation/include/CoreLocation/CLLocationManager.h index 9e0fa55c4..993124e76 100644 --- a/src/frameworks/CoreLocation/include/CoreLocation/CLLocationManager.h +++ b/src/frameworks/CoreLocation/include/CoreLocation/CLLocationManager.h @@ -20,5 +20,14 @@ #include @interface CLLocationManager : NSObject +{ + id _delegate; +} + +/* darling-lol: real property so callers (Electron's device-permission + * path) get a proper accessor instead of falling through to the stub + * forwardInvocation, whose 0-arg signature raised + * `NSForwardSignatureError: invoked with 3 args, but 2 expected`. */ +@property (nonatomic, assign) id delegate; @end diff --git a/src/frameworks/CoreLocation/src/CLLocation.m b/src/frameworks/CoreLocation/src/CLLocation.m index f396e00e4..5f3965705 100644 --- a/src/frameworks/CoreLocation/src/CLLocation.m +++ b/src/frameworks/CoreLocation/src/CLLocation.m @@ -32,3 +32,10 @@ } @end + +const CLLocationAccuracy kCLLocationAccuracyBestForNavigation = -2.0; +const CLLocationAccuracy kCLLocationAccuracyBest = -1.0; +const CLLocationAccuracy kCLLocationAccuracyNearestTenMeters = 10.0; +const CLLocationAccuracy kCLLocationAccuracyHundredMeters = 100.0; +const CLLocationAccuracy kCLLocationAccuracyKilometer = 1000.0; +const CLLocationAccuracy kCLLocationAccuracyThreeKilometers = 3000.0; diff --git a/src/frameworks/CoreLocation/src/CLLocationManager.m b/src/frameworks/CoreLocation/src/CLLocationManager.m index 0b927c40e..d186494e7 100644 --- a/src/frameworks/CoreLocation/src/CLLocationManager.m +++ b/src/frameworks/CoreLocation/src/CLLocationManager.m @@ -21,6 +21,8 @@ @implementation CLLocationManager +@synthesize delegate = _delegate; + - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { return [NSMethodSignature signatureWithObjCTypes: "v@:"]; diff --git a/src/frameworks/CoreMedia/src/CoreMedia.c b/src/frameworks/CoreMedia/src/CoreMedia.c index 3d76e8103..4a975959c 100644 --- a/src/frameworks/CoreMedia/src/CoreMedia.c +++ b/src/frameworks/CoreMedia/src/CoreMedia.c @@ -26,6 +26,32 @@ const CFStringRef kCMFormatDescriptionExtension_SampleDescriptionExtensionAtoms = CFSTR("SampleDescriptionExtensionAtoms"); const CFStringRef kCMSampleAttachmentKey_NotSync = CFSTR("NotSync"); +const CFStringRef kCMFormatDescriptionTransferFunction_ITU_R_709_2 = CFSTR("ITU_R_709_2"); +const CFStringRef kCMFormatDescriptionTransferFunction_SMPTE_240M_1995 = CFSTR("SMPTE_240M_1995"); +const CFStringRef kCMFormatDescriptionTransferFunction_UseGamma = CFSTR("UseGamma"); +const CFStringRef kCMFormatDescriptionTransferFunction_ITU_R_2020 = CFSTR("ITU_R_2020"); +const CFStringRef kCMFormatDescriptionTransferFunction_ITU_R_2100_HLG = CFSTR("ITU_R_2100_HLG"); +const CFStringRef kCMFormatDescriptionTransferFunction_SMPTE_ST_2084_PQ = CFSTR("SMPTE_ST_2084_PQ"); +const CFStringRef kCMFormatDescriptionTransferFunction_SMPTE_ST_428_1 = CFSTR("SMPTE_ST_428_1"); +const CFStringRef kCMFormatDescriptionTransferFunction_Linear = CFSTR("Linear"); +const CFStringRef kCMFormatDescriptionTransferFunction_sRGB = CFSTR("sRGB"); +const CFStringRef kCMFormatDescriptionColorPrimaries_ITU_R_709_2 = CFSTR("ITU_R_709_2"); +const CFStringRef kCMFormatDescriptionColorPrimaries_EBU_3213 = CFSTR("EBU_3213"); +const CFStringRef kCMFormatDescriptionColorPrimaries_SMPTE_C = CFSTR("SMPTE_C"); +const CFStringRef kCMFormatDescriptionColorPrimaries_ITU_R_2020 = CFSTR("ITU_R_2020"); +const CFStringRef kCMFormatDescriptionColorPrimaries_P3_D65 = CFSTR("P3_D65"); +const CFStringRef kCMFormatDescriptionColorPrimaries_DCI_P3 = CFSTR("DCI_P3"); +const CFStringRef kCMFormatDescriptionExtension_ContentLightLevelInfo = CFSTR("ContentLightLevelInfo"); +const CFStringRef kCMFormatDescriptionExtension_Depth = CFSTR("Depth"); +const CFStringRef kCMFormatDescriptionExtension_FormatName = CFSTR("FormatName"); +const CFStringRef kCMFormatDescriptionExtension_FullRangeVideo = CFSTR("FullRangeVideo"); +const CFStringRef kCMFormatDescriptionExtension_MasteringDisplayColorVolume = CFSTR("MasteringDisplayColorVolume"); +const CFStringRef kCMSampleAttachmentKey_IsDependedOnByOthers = CFSTR("IsDependedOnByOthers"); +const CFStringRef kCMFormatDescriptionYCbCrMatrix_ITU_R_709_2 = CFSTR("ITU_R_709_2"); +const CFStringRef kCMFormatDescriptionYCbCrMatrix_ITU_R_601_4 = CFSTR("ITU_R_601_4"); +const CFStringRef kCMFormatDescriptionYCbCrMatrix_SMPTE_240M_1995 = CFSTR("SMPTE_240M_1995"); +const CFStringRef kCMFormatDescriptionYCbCrMatrix_ITU_R_2020 = CFSTR("ITU_R_2020"); + static int verbose = 0; __attribute__((constructor)) diff --git a/src/frameworks/CoreServices/CMakeLists.txt b/src/frameworks/CoreServices/CMakeLists.txt index 3fac3be55..aae2c7435 100644 --- a/src/frameworks/CoreServices/CMakeLists.txt +++ b/src/frameworks/CoreServices/CMakeLists.txt @@ -82,6 +82,7 @@ reexport(CoreServices Metadata ${Metadata_BUILD}) reexport(CoreServices SearchKit ${SearchKit_BUILD}) reexport(CoreServices SharedFileList ${SharedFileList_BUILD}) reexport(CoreServices OSServices ${OSServices_BUILD}) +reexport(CoreServices Foundation ${CMAKE_BINARY_DIR}/src/external/foundation/Foundation) set_property(TARGET CoreServices APPEND_STRING PROPERTY LINK_FLAGS " -Wl,-reexported_symbols_list,${CMAKE_CURRENT_SOURCE_DIR}/reexport.exp") diff --git a/src/frameworks/CoreServices/src/CarbonCore/UnicodeUtilities.cpp b/src/frameworks/CoreServices/src/CarbonCore/UnicodeUtilities.cpp index 094a9483d..84810f6ab 100644 --- a/src/frameworks/CoreServices/src/CarbonCore/UnicodeUtilities.cpp +++ b/src/frameworks/CoreServices/src/CarbonCore/UnicodeUtilities.cpp @@ -212,8 +212,10 @@ OSStatus UCKeyTranslate(const UCKeyboardLayout *keyLayoutPtr, UInt16 virtualKeyC const UCKeyToCharTableIndex* tableIndex = (UCKeyToCharTableIndex*) (((char*) keyLayoutPtr) + keyLayoutPtr->keyboardTypeList[0].keyToCharTableIndexOffset); uint8_t tableNumber = modMaps->defaultTableNum; - if ((modifierKeyState >> 8) < modMaps->modifiersCount) - tableNumber = modMaps->tableNum[(modifierKeyState >> 8)]; + // UCKeyTranslate receives the modifier byte after the caller shifts the + // Carbon EventRecord flags. Shifting again loses Shift and Option state. + if (modifierKeyState < modMaps->modifiersCount) + tableNumber = modMaps->tableNum[modifierKeyState]; UniChar c; if (virtualKeyCode >= tableIndex->keyToCharTableSize || tableNumber >= tableIndex->keyToCharTableCount) diff --git a/src/frameworks/CoreServices/src/FSEvents/FSEventsImpl.m b/src/frameworks/CoreServices/src/FSEvents/FSEventsImpl.m index cf9933a84..6651d50dc 100644 --- a/src/frameworks/CoreServices/src/FSEvents/FSEventsImpl.m +++ b/src/frameworks/CoreServices/src/FSEvents/FSEventsImpl.m @@ -23,6 +23,7 @@ #include #include #include +#include #include static dispatch_queue_t g_fsEventsQueue = NULL; @@ -68,6 +69,24 @@ static void rlPerform(void* info) for (NSString* path in _pathsToWatch) { int wd = inotify_add_watch(_fd, [path UTF8String], IN_ATTRIB | IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE); + /* darling-lol: a path that doesn't exist yet (e.g. /etc/hosts or + * /etc/resolv.conf missing in the prefix) makes inotify_add_watch + * return -1. Apple's FSEvents tolerates unwatchable paths and the + * stream still starts; Chromium (dns_config_service_posix) treats + * a failed start as fatal for its DNS watcher. Skip failures + * instead of inserting a bogus -1 wd, and remember the path so a + * later event on the directory can still be reported. */ + if (wd < 0) + { + if (getenv("DARLING_LOL_FSEVLOG") != NULL) + { + char b[512]; + int n = snprintf(b, sizeof(b), "FSEV add_watch failed: %s (errno=%d)\n", + [path UTF8String], errno); + if (n > 0) write(2, b, n); + } + continue; + } [wdmap setObject: path forKey: [NSNumber numberWithInt: wd]]; } diff --git a/src/frameworks/CoreServices/src/LaunchServices/LSOpen.m b/src/frameworks/CoreServices/src/LaunchServices/LSOpen.m index dd78f6df4..8fa95a174 100644 --- a/src/frameworks/CoreServices/src/LaunchServices/LSOpen.m +++ b/src/frameworks/CoreServices/src/LaunchServices/LSOpen.m @@ -19,8 +19,104 @@ along with Darling. If not, see . #include +#include #include +#include "LSHostOpen.h" #include +#include +#include +#include + +// darling-lol M2a: minimal bundle launching. +// Supports: file URLs (or bare POSIX paths) pointing at .app bundles, +// optionally with an explicit target app via inAppParams->application. +// The bundle's executable is spawned directly with the emulated posix_spawn +// (no launchd job registration yet — see _LSLaunchApplication). +// NOT yet supported (documented deviations): +// - AppleEvent delivery (oapp/odoc): the app is spawned bare, as if launched +// by hand. Apps waiting for their initial event will park. Needs #425/#937. +// - inAppParams->argv / ->environment: not propagated (_LSLaunchApplication +// takes no argv yet). +// - inRole / launch flags (kLSLaunchNewInstance etc.): every launch spawns a +// new instance; no running-app registry, so outPSNs is left untouched. +// - non-bundle documents without an explicit application: there is no default +// handler database yet (needs UTI/LaunchServices DB). +static OSStatus +LSOpenOneURL(CFURLRef url, const LSApplicationParameters *params) +{ + CFStringRef pathStr; + char path[4096]; + struct stat st; + CFURLRef appURL = NULL; + OSStatus status; + + if (url == NULL) + return paramErr; + + if ((params == NULL || params->application == NULL) && _LSIsHostOpenURL(url)) + return _LSOpenURLOnHost(url); + + // open(1) builds URLs via +URLWithString from a raw path, which may yield + // a scheme-less URL object. CFURLCopyFileSystemPath works either way. + pathStr = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle); + if (pathStr == NULL) + return paramErr; + + if (!CFStringGetCString(pathStr, path, sizeof(path), kCFStringEncodingUTF8)) + { + CFRelease(pathStr); + return paramErr; + } + CFRelease(pathStr); + + if (params != NULL && params->application != NULL) + { + // Explicit target application wins; the URLs become its documents + // (delivered as argv once M2c lands; for now they are dropped). + char appPath[4096]; + CFStringRef appStr; + status = FSRefMakePath(params->application, (UInt8*) appPath, sizeof(appPath) - 1); + if (status != noErr) + return status; + appStr = CFStringCreateWithCString(NULL, appPath, kCFStringEncodingUTF8); + appURL = CFURLCreateWithFileSystemPath(NULL, appStr, kCFURLPOSIXPathStyle, TRUE); + CFRelease(appStr); + } + else + { + size_t len = strlen(path); + Boolean looksLikeBundle = FALSE; + + if (len > 4 && strcasecmp(path + len - 4, ".app") == 0) + looksLikeBundle = TRUE; + else if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) + { + // Bundle without .app suffix (e.g. helper apps, frameworks-as-apps). + char plist[4096 + 32]; + snprintf(plist, sizeof(plist), "%s/Contents/Info.plist", path); + if (stat(plist, &st) == 0) + looksLikeBundle = TRUE; + } + + if (!looksLikeBundle) + return kLSApplicationNotFoundErr; // no default-handler DB yet + + CFStringRef tmp = CFStringCreateWithCString(NULL, path, kCFStringEncodingUTF8); + appURL = CFURLCreateWithFileSystemPath(NULL, tmp, kCFURLPOSIXPathStyle, TRUE); + CFRelease(tmp); + } + + if (appURL == NULL) + return paramErr; + + status = _LSLaunchApplication(appURL); + CFRelease(appURL); + + // _LSLaunchApplication returns 1 on failure instead of a MacErrors code. + if (status != noErr) + status = fnfErr; + return status; +} OSStatus LSOpenURLsWithRole( @@ -31,8 +127,27 @@ LSOpenURLsWithRole( ProcessSerialNumber * outPSNs, CFIndex inMaxPSNCount) { - puts("LSOpenURLsWithRole STUB"); - return unimpErr; + CFIndex count, i, launched = 0; + OSStatus status, firstErr = noErr; + + (void) inRole; + (void) inAEParam; + (void) outPSNs; + (void) inMaxPSNCount; + + if (inURLs == NULL || (count = CFArrayGetCount(inURLs)) <= 0) + return paramErr; + + for (i = 0; i < count; i++) + { + status = LSOpenOneURL(CFArrayGetValueAtIndex(inURLs, i), inAppParams); + if (status == noErr) + launched++; + else if (launched == 0 && firstErr == noErr) + firstErr = status; + } + + return launched > 0 ? noErr : firstErr; } OSStatus diff --git a/src/frameworks/CoreServices/src/LaunchServices/LaunchServices.c b/src/frameworks/CoreServices/src/LaunchServices/LaunchServices.c index 854fa1e7f..8a76923c4 100644 --- a/src/frameworks/CoreServices/src/LaunchServices/LaunchServices.c +++ b/src/frameworks/CoreServices/src/LaunchServices/LaunchServices.c @@ -19,9 +19,14 @@ #include #include +#include "LSHostOpen.h" #include #include -#include +#include +#include +#include + +extern char** environ; static void get_main_executable_info(CFURLRef appURL, CFStringRef *exec, CFStringRef *bundleName); @@ -77,6 +82,12 @@ OSStatus LSOpenFromURLSpec(const LSLaunchURLSpec *inLaunchSpec, CFURLRef *outLau if (extension != NULL) CFRelease(extension); } + else if (_LSIsHostOpenURL(url)) + { + temp = _LSOpenURLOnHost(url); + if (temp) + ret = temp; + } else { printf("We can't handle this scheme yet\n"); @@ -108,8 +119,14 @@ OSStatus _LSLaunchApplication(CFURLRef appPath) CFRelease(url_plus_exec); url_plus_exec = CFURLCreateCopyAppendingPathComponent(kCFAllocatorDefault, pre, executable, FALSE); CFRelease(pre); - /* Launch the executable */ - executable = CFURLGetString(url_plus_exec); + /* Launch the executable. NB: CFURLGetString would give back a + * percent-encoded file:// URL, which is not spawnable; extract the + * POSIX path instead. */ + CFRelease(executable); + executable = CFURLCopyFileSystemPath(url_plus_exec, kCFURLPOSIXPathStyle); + CFRelease(url_plus_exec); + if (executable == NULL) + return 1; /* Try to use CFBundleName first */ if (bundleName == NULL) @@ -122,15 +139,19 @@ OSStatus _LSLaunchApplication(CFURLRef appPath) CFStringGetFileSystemRepresentation(executable, exec, execSize); argv[0] = exec; argv[1] = NULL; - pid = spawn_via_launchd(name, (const char *const *)argv, NULL); - if (pid < 0) + /* Launch the executable directly. The macOS-correct route is + * spawn_via_launchd (launchd job registration, -W waiting, PSN + * tracking), but Darling's vproc_mig_spawn2 currently fails the + * request (EIO) and nothing in-tree exercises it, so spawn the + * child ourselves via the emulated posix_spawn. */ + int err = posix_spawn(&pid, exec, NULL, NULL, argv, environ); + if (err != 0) { - printf("%s: Failed to spawn via launchd\n", exec); + printf("%s: Failed to posix_spawn: %s\n", exec, strerror(err)); return 1; } free(name); free(exec); - CFRelease(url_plus_exec); CFRelease(executable); } @@ -184,3 +205,25 @@ err_pre_info: CFRelease(rs); CFRelease(info_plist_url); } + +OSStatus LSSetDefaultHandlerForURLScheme(CFStringRef inURLScheme, CFStringRef inHandlerBundleID) +{ + /* Darling has no shared LaunchServices database yet; accept and + * ignore (callers such as Discord's URL-scheme registration only + * need the call to succeed). */ + (void)inURLScheme; + (void)inHandlerBundleID; + if (verbose) + printf("STUB: LSSetDefaultHandlerForURLScheme\n"); + return noErr; +} + +OSStatus LSSetDefaultRoleHandlerForContentType(CFStringRef inContentType, LSRolesMask inRole, CFStringRef inHandlerBundleID) +{ + (void)inContentType; + (void)inRole; + (void)inHandlerBundleID; + if (verbose) + printf("STUB: LSSetDefaultRoleHandlerForContentType\n"); + return noErr; +} diff --git a/src/frameworks/CoreVideo/include/CoreVideo/CVBuffer.h b/src/frameworks/CoreVideo/include/CoreVideo/CVBuffer.h index 09b70e4cb..2f7f5b89a 100644 --- a/src/frameworks/CoreVideo/include/CoreVideo/CVBuffer.h +++ b/src/frameworks/CoreVideo/include/CoreVideo/CVBuffer.h @@ -8,6 +8,8 @@ __BEGIN_DECLS typedef struct CV_BRIDGED_TYPE(id) __CVBuffer* CVBufferRef; +extern const CFStringRef kCVBufferPropagatedAttachmentsKey; + __END_DECLS #endif // _COREVIDEO_CVBUFFER_H_ diff --git a/src/frameworks/CoreVideo/include/CoreVideo/CVImageBuffer.h b/src/frameworks/CoreVideo/include/CoreVideo/CVImageBuffer.h index 937ba8312..45ab80e75 100644 --- a/src/frameworks/CoreVideo/include/CoreVideo/CVImageBuffer.h +++ b/src/frameworks/CoreVideo/include/CoreVideo/CVImageBuffer.h @@ -30,6 +30,7 @@ extern const CFStringRef kCVImageBufferTransferFunction_Linear; extern const CFStringRef kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ; extern const CFStringRef kCVImageBufferTransferFunction_SMPTE_ST_428_1; extern const CFStringRef kCVImageBufferTransferFunction_SMPTE_C; +extern const CFStringRef kCVImageBufferTransferFunction_sRGB; extern CFStringRef const kCVImageBufferPixelAspectRatioHorizontalSpacingKey; extern CFStringRef const kCVImageBufferPixelAspectRatioVerticalSpacingKey; @@ -43,5 +44,7 @@ extern CFStringRef const kCVImageBufferColorPrimaries_SMPTE_C; extern CFStringRef const kCVImageBufferTransferFunction_ITU_R_709_2; extern CFStringRef const kCVImageBufferTransferFunction_SMPTE_240M_1995; extern CFStringRef const kCVImageBufferTransferFunction_UseGamma; +extern const CFStringRef kCVImageBufferContentLightLevelInfoKey; +extern const CFStringRef kCVImageBufferMasteringDisplayColorVolumeKey; #endif // _COREVIDEO_CVIMAGEBUFFER_H_ diff --git a/src/frameworks/CoreVideo/include/CoreVideo/CVPixelBufferPool.h b/src/frameworks/CoreVideo/include/CoreVideo/CVPixelBufferPool.h index 299caa5b1..bdff6028c 100644 --- a/src/frameworks/CoreVideo/include/CoreVideo/CVPixelBufferPool.h +++ b/src/frameworks/CoreVideo/include/CoreVideo/CVPixelBufferPool.h @@ -26,6 +26,7 @@ __BEGIN_DECLS extern const CFStringRef kCVPixelBufferPoolMaximumBufferAgeKey; +extern const CFStringRef kCVPixelBufferPoolAllocationThresholdKey; extern const CFStringRef kCVPixelBufferPoolMinimumBufferCountKey; __END_DECLS diff --git a/src/frameworks/CoreVideo/src/CVDisplayLink.mm b/src/frameworks/CoreVideo/src/CVDisplayLink.mm index 3e9cba226..99e0a5494 100644 --- a/src/frameworks/CoreVideo/src/CVDisplayLink.mm +++ b/src/frameworks/CoreVideo/src/CVDisplayLink.mm @@ -1,38 +1,887 @@ #include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #import #import #import #import +#import #include #include +/* + * M58b FrameSink bridge. + * + * Chromium 108's viz source does keep receiving the display ticks, but its + * CompositorFrameSinkSupport stops forwarding them after the first no-damage + * response. Under Darling the support object also crosses a guest-memory + * boundary before the timing-details map is serialized. When we deliberately + * keep the source alive, that stale map can otherwise reach Mojo and crash the + * GPU process. Keep this compatibility bridge here, beside the display pump, + * so it is installed in every Electron process that uses this shim. + * + * These are guarded offsets for Electron 22.3.27 / Chromium 108. Every patch + * checks the original bytes and fails closed when a different framework is + * loaded. The bridge is intentionally limited to the known framework image. + */ +extern "C" { +int mprotect(void *, unsigned long, int); +unsigned _dyld_image_count(void); +const char *_dyld_get_image_name(unsigned); +const char *_dyld_get_image_header(unsigned); +} + +static const unsigned long kCFSSShouldSendVA = 0x64fb760UL; +static const unsigned long kDirectClientOnBeginVA = 0x141bdc0UL; +static const unsigned long kBundleClientOnBeginVA = 0x64fdfd0UL; +static const unsigned long kRendererBindToClientVA = 0x5e21c10UL; +static const unsigned long kRendererOnBeginVA = 0x5e23140UL; +static const unsigned long kNullDetailsThunkVA = 0x64cde60UL; +static const unsigned long kTimingCopyCallVA = 0x64cdd96UL; +static const unsigned long kTimingDefaultCtorVA = 0x0eeb080UL; +static const unsigned long kContextCopyCallVA = 0x64cdd76UL; +static const unsigned long kContextDefaultCtorVA = 0x0ef1d20UL; +static const unsigned long kInlineCopyCallVA = 0x64cdd86UL; +static void frameSinkTrace(const char *tag, unsigned long value); + +static void (*gNullDetailsThunkOriginal)(void *, void *, void *) = nullptr; + +static void zeroInlineDetails(void *destination, const void *source) { + unsigned char zeros[0xa0] = {}; + int fd = open("/Volumes/SystemRoot/proc/self/mem", O_WRONLY, 0); + if (fd >= 0) { + pwrite(fd, zeros, sizeof(zeros), (off_t)(unsigned long)destination); + close(fd); + } +} + +static bool mappedRange(const void *pointer, size_t size) { + if (pointer == nullptr || size == 0) + return false; + unsigned long start = (unsigned long)pointer & ~0xfffUL; + unsigned long end = ((unsigned long)pointer + size + 0xfffUL) & ~0xfffUL; + for (unsigned long page = start; page < end; page += 0x1000) { + char resident = 0; + if (mincore((void *)page, 0x1000, &resident) != 0) + return false; + } + return true; +} + +static void nullDetailsThunkGuard(void *block, void *arg, void *details) { + /* This thunk serializes compositor timing metadata. Under Darling its + * nested vectors refer to the producer's guest-memory view. Preserve the + * completion callback but substitute a coherent empty details payload. */ + if (details == nullptr) + return; + gNullDetailsThunkOriginal(block, arg, details); +} + +static bool installNullDetailsThunkGuard(unsigned char *base) { + unsigned char *target = base + kNullDetailsThunkVA; + static const unsigned char expected[15] = { + 0x55, 0x48, 0x89, 0xe5, 0x48, 0x89, 0xd0, 0x48, + 0x8b, 0x52, 0x28, 0x48, 0x8b, 0x48, 0x30 + }; + if (memcmp(target, expected, sizeof(expected)) != 0) + return false; + unsigned char *copyCall = base + kTimingCopyCallVA; + static const unsigned char expectedCopyCall[5] = { + 0xe8, 0x95, 0xd6, 0xa1, 0xfa + }; + unsigned char *contextCall = base + kContextCopyCallVA; + unsigned char *inlineCall = base + kInlineCopyCallVA; + static const unsigned char expectedContextCall[5] = { + 0xe8, 0x05, 0x40, 0xa2, 0xfa + }; + static const unsigned char expectedInlineCall[5] = { + 0xe8, 0x55, 0x9c, 0xa1, 0xfa + }; + if (memcmp(copyCall, expectedCopyCall, sizeof(expectedCopyCall)) != 0) + return false; + if (memcmp(contextCall, expectedContextCall, + sizeof(expectedContextCall)) != 0) + return false; + if (memcmp(inlineCall, expectedInlineCall, sizeof(expectedInlineCall)) != 0) + return false; + + unsigned char *trampoline = (unsigned char *)mmap(nullptr, 4096, + PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANON, -1, 0); + if (trampoline == MAP_FAILED) + return false; + memcpy(trampoline, expected, sizeof(expected)); + unsigned char *jump = trampoline + sizeof(expected); + jump[0] = 0x48; + jump[1] = 0xb8; + unsigned long resume = (unsigned long)(target + sizeof(expected)); + memcpy(jump + 2, &resume, sizeof(resume)); + jump[10] = 0xff; + jump[11] = 0xe0; + gNullDetailsThunkOriginal = + (void (*)(void *, void *, void *))trampoline; + + unsigned long page = (unsigned long)target & ~0xfffUL; + if (mprotect((void *)page, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) + return false; + int32_t relativeCtor = (int32_t)(kTimingDefaultCtorVA - + (kTimingCopyCallVA + sizeof(expectedCopyCall))); + copyCall[0] = 0xe8; + memcpy(copyCall + 1, &relativeCtor, sizeof(relativeCtor)); + int32_t relativeContextCtor = (int32_t)(kContextDefaultCtorVA - + (kContextCopyCallVA + sizeof(expectedContextCall))); + contextCall[0] = 0xe8; + memcpy(contextCall + 1, &relativeContextCtor, + sizeof(relativeContextCtor)); + int64_t inlineDistance = (unsigned long)&zeroInlineDetails - + (unsigned long)(inlineCall + sizeof(expectedInlineCall)); + if (inlineDistance < INT32_MIN || inlineDistance > INT32_MAX) + return false; + int32_t relativeInlineHelper = (int32_t)inlineDistance; + inlineCall[0] = 0xe8; + memcpy(inlineCall + 1, &relativeInlineHelper, + sizeof(relativeInlineHelper)); + unsigned char detour[12] = { 0x48, 0xb8 }; + unsigned long guard = (unsigned long)&nullDetailsThunkGuard; + memcpy(detour + 2, &guard, sizeof(guard)); + detour[10] = 0xff; + detour[11] = 0xe0; + memcpy(target, detour, sizeof(detour)); + memset(target + sizeof(detour), 0x90, + sizeof(expected) - sizeof(detour)); + return true; +} + +struct FrameSinkHook { + unsigned char *target; + unsigned char saved[12]; + unsigned long replacement; + volatile int lock; + bool installed; +}; + +struct FrameSinkSavedState { + uint32_t magic; + unsigned char direct[12]; + unsigned char bundle[12]; +}; + +struct RendererHookSavedState { + uint32_t magic; + unsigned char saved[12]; +}; + +struct RendererBeginState { + uint32_t magic; + uint32_t running; + unsigned long sink; + uint64_t sequence; +}; + +static const uint32_t kFrameSinkSavedMagic = 0x4d353846u; + +static FrameSinkHook gDirectClientHook = {}; +static FrameSinkHook gBundleClientHook = {}; +static FrameSinkHook gRendererNeedsHook = {}; +static unsigned char gEmptyFrameTimingDetails[24] = {}; + +static const uint32_t kRendererHookMagic = 0x4d353853u; +static const uint32_t kRendererBeginMagic = 0x4d353842u; + +static bool frameSinkPrologueIsUsable(const unsigned char *prologue); + +static void frameSinkTrace(const char *tag, unsigned long value) { + char line[96]; + int len = snprintf(line, sizeof(line), "FS pid=%d %s %lx\n", (int)getpid(), + tag, value); + int fd = open("/Volumes/SystemRoot/tmp/opencode/framesink-bridge.log", + O_WRONLY | O_CREAT | O_APPEND, 0644); + if (fd >= 0) { + write(fd, line, (size_t)len); + close(fd); + } +} + +static const char *rendererHookStatePath(void) { + static char path[128]; + static bool initialized = false; + if (!initialized) { + snprintf(path, sizeof(path), "/dev/shm/darling-renderer-hook-%d.state", + (int)getpid()); + initialized = true; + } + return path; +} + +static const char *rendererBeginStatePath(void) { + static char path[128]; + static bool initialized = false; + if (!initialized) { + snprintf(path, sizeof(path), "/dev/shm/darling-renderer-begin-%d.state", + (int)getpid()); + initialized = true; + } + return path; +} + +static bool rendererHookStateIsUsable(void) { + RendererHookSavedState state = {}; + int fd = open(rendererHookStatePath(), O_RDONLY, 0); + if (fd < 0) + return false; + ssize_t got = read(fd, &state, sizeof(state)); + close(fd); + return got == (ssize_t)sizeof(state) && + state.magic == kRendererHookMagic && + frameSinkPrologueIsUsable(state.saved); +} + +static void rendererWriteState(void *sink, bool running) { + RendererBeginState state = {}; + int fd = open(rendererBeginStatePath(), O_RDWR | O_CREAT, 0644); + if (fd < 0) + return; + if (flock(fd, LOCK_EX) == 0) { + lseek(fd, 0, SEEK_SET); + if (read(fd, &state, sizeof(state)) != (ssize_t)sizeof(state) || + state.magic != kRendererBeginMagic) + memset(&state, 0, sizeof(state)); + state.magic = kRendererBeginMagic; + state.sink = (unsigned long)sink; + state.running = running ? 1 : 0; + lseek(fd, 0, SEEK_SET); + ftruncate(fd, 0); + write(fd, &state, sizeof(state)); + flock(fd, LOCK_UN); + } + close(fd); +} + +static bool rendererReadState(RendererBeginState *state) { + int fd = open(rendererBeginStatePath(), O_RDONLY, 0); + if (fd < 0) + return false; + ssize_t got = read(fd, state, sizeof(*state)); + close(fd); + return got == (ssize_t)sizeof(*state) && + state->magic == kRendererBeginMagic && state->running != 0 && + state->sink != 0; +} + +static unsigned char *frameSinkFindFramework(void) { + for (unsigned i = 0; i < _dyld_image_count(); ++i) { + const char *name = _dyld_get_image_name(i); + if (name && strstr(name, "Versions/A/Electron Framework")) + return (unsigned char *) _dyld_get_image_header(i); + } + return nullptr; +} + +static const char *frameSinkStatePath(void) { + static char path[128]; + static bool initialized = false; + if (!initialized) { + snprintf(path, sizeof(path), "/dev/shm/darling-framesink-%d.state", + (int) getpid()); + initialized = true; + } + return path; +} + +static bool frameSinkReadSaved(FrameSinkSavedState *state) { + int fd = open(frameSinkStatePath(), 0, 0); + if (fd < 0) + return false; + ssize_t got = read(fd, state, sizeof(*state)); + close(fd); + return got == (ssize_t) sizeof(*state) && + state->magic == kFrameSinkSavedMagic; +} + +static bool frameSinkStateIsUsable(void) { + FrameSinkSavedState state = {}; + if (!frameSinkReadSaved(&state)) + return false; + /* A record containing only the direct prologue is an old/partial record. + * PID reuse makes that case common: the first CoreVideo image wrote its + * direct hook, then another image observed the file and skipped the bundle + * hook. Never let a zero bundle prologue be used to restore executable + * code. */ + for (unsigned i = 0; i < sizeof(state.direct); ++i) + if (state.direct[i] == 0) + return false; + for (unsigned i = 0; i < sizeof(state.bundle); ++i) + if (state.bundle[i] == 0) + return false; + return true; +} + +static bool frameSinkPrologueIsUsable(const unsigned char *prologue) { + /* Both Chromium methods begin with a frame-pointer prologue; the remaining + * bytes are version-specific but must not be all zero. */ + if (prologue[0] != 0x55 || prologue[1] != 0x48 || + prologue[2] != 0x89 || prologue[3] != 0xe5) + return false; + for (unsigned i = 4; i < 12; ++i) + if (prologue[i] != 0) + return true; + return false; +} + +static void frameSinkWriteJump(unsigned char *target, unsigned long replacement) { + unsigned char jump[12]; + jump[0] = 0x48; + jump[1] = 0xb8; + for (unsigned i = 0; i < 8; ++i) + jump[i + 2] = (replacement >> (i * 8)) & 0xff; + jump[10] = 0xff; + jump[11] = 0xe0; + for (unsigned i = 0; i < sizeof(jump); ++i) + target[i] = jump[i]; +} + +static bool frameSinkPatchBytes(unsigned char *at, const unsigned char *want, + unsigned count, const unsigned char *replacement) { + for (unsigned i = 0; i < count; ++i) + if (at[i] != want[i]) + return false; + unsigned long page = (unsigned long) at & ~0xfffUL; + if (mprotect((void *) page, 0x1000, 7) != 0) + return false; + for (unsigned i = 0; i < count; ++i) + at[i] = replacement[i]; + return true; +} + +static void frameSinkHookPatch(FrameSinkHook *hook, bool enabled) { + unsigned char jump[12]; + unsigned long page = (unsigned long) hook->target & ~0xfffUL; + mprotect((void *) page, 0x1000, 7); + if (!enabled) { + for (unsigned i = 0; i < sizeof(hook->saved); ++i) + hook->target[i] = hook->saved[i]; + return; + } + jump[0] = 0x48; + jump[1] = 0xb8; + for (unsigned i = 0; i < 8; ++i) + jump[i + 2] = (hook->replacement >> (i * 8)) & 0xff; + jump[10] = 0xff; + jump[11] = 0xe0; + for (unsigned i = 0; i < sizeof(jump); ++i) + hook->target[i] = jump[i]; +} + +static bool frameSinkInstallHook(FrameSinkHook *hook, unsigned char *target, + void *replacement) { + /* Do not overwrite an already-installed copy of this bridge. */ + if (target[0] == 0x48 && target[1] == 0xb8) + return false; + /* All three Chromium functions have the normal x86_64 frame prologue. */ + if (target[0] != 0x55 || target[1] != 0x48 || target[2] != 0x89 || + target[3] != 0xe5) + return false; + hook->target = target; + hook->replacement = (unsigned long) replacement; + for (unsigned i = 0; i < sizeof(hook->saved); ++i) + hook->saved[i] = target[i]; + frameSinkHookPatch(hook, true); + hook->installed = true; + return true; +} + +static void frameSinkCallOriginal(unsigned long offset, unsigned long replacement, + bool bundle, void *self, void *args, void *details) { + /* Do not dereference FrameSinkHook or any other shim global here. This + * function is entered from Chromium's callback thread, whose guest view is + * not the constructor's view under Darling. */ + unsigned char *base = frameSinkFindFramework(); + FrameSinkSavedState saved = {}; + int fd = open(frameSinkStatePath(), O_RDWR, 0); + if (fd < 0 || !base) + return; + if (flock(fd, LOCK_EX) != 0) { + close(fd); + return; + } + lseek(fd, 0, 0); + if (read(fd, &saved, sizeof(saved)) != (ssize_t) sizeof(saved) || + saved.magic != kFrameSinkSavedMagic || + !frameSinkPrologueIsUsable(bundle ? saved.bundle : saved.direct)) { + frameSinkTrace(bundle ? "bad-bundle-state" : "bad-direct-state", + (unsigned long)details); + flock(fd, LOCK_UN); + close(fd); + return; + } + unsigned char *target = base + offset; + const unsigned char *prologue = bundle ? saved.bundle : saved.direct; + unsigned long page = (unsigned long) target & ~0xfffUL; + if (mprotect((void *) page, 0x1000, 7) != 0) { + flock(fd, LOCK_UN); + close(fd); + return; + } + for (unsigned i = 0; i < 12; ++i) + target[i] = prologue[i]; + ((void (*)(void *, void *, void *)) target)(self, args, details); + frameSinkWriteJump(target, replacement); + flock(fd, LOCK_UN); + close(fd); +} + +static void frameSinkDirectOnBegin(void *self, void *args, void *details) { + frameSinkCallOriginal(kDirectClientOnBeginVA, + (unsigned long) frameSinkDirectOnBegin, false, self, args, details); +} + +static void frameSinkBundleOnBegin(void *self, void *args, void *details) { + frameSinkCallOriginal(kBundleClientOnBeginVA, + (unsigned long) frameSinkBundleOnBegin, true, self, args, details); +} + +static bool frameSinkRendererBindToClient(void *self, void *client) { + /* BindToClient runs in every renderer even when OnNeedsBeginFrames never + * does under Darling. Capturing here breaks that circular dependency. */ + frameSinkTrace("renderer-bind-entry", (unsigned long)self); + rendererWriteState(self, true); + frameSinkTrace("renderer-bind-saved", (unsigned long)client); + unsigned char *base = frameSinkFindFramework(); + RendererHookSavedState saved = {}; + int fd = open(rendererHookStatePath(), O_RDWR, 0); + if (fd < 0 || !base) + return false; + if (flock(fd, LOCK_EX) != 0) { + close(fd); + return false; + } + lseek(fd, 0, SEEK_SET); + if (read(fd, &saved, sizeof(saved)) != (ssize_t)sizeof(saved) || + saved.magic != kRendererHookMagic || + !frameSinkPrologueIsUsable(saved.saved)) { + flock(fd, LOCK_UN); + close(fd); + return false; + } + unsigned char *target = base + kRendererBindToClientVA; + unsigned long page = (unsigned long)target & ~0xfffUL; + bool result = false; + if (mprotect((void *)page, 0x1000, 7) == 0) { + for (unsigned i = 0; i < sizeof(saved.saved); ++i) + target[i] = saved.saved[i]; + frameSinkTrace("renderer-bind-call", (unsigned long)target); + result = ((bool (*)(void *, void *))target)(self, client); + frameSinkWriteJump(target, + (unsigned long)frameSinkRendererBindToClient); + } + flock(fd, LOCK_UN); + close(fd); + frameSinkTrace("renderer-bind", (unsigned long)self); + return result; +} + +static void rendererBeginTimerF(CFRunLoopTimerRef timer, void *info) { + (void)timer; + (void)info; + RendererBeginState state = {}; + if (!rendererReadState(&state)) + return; + unsigned char *base = frameSinkFindFramework(); + if (!base) + return; + + struct mach_timebase_info timebase = {}; + mach_timebase_info(&timebase); + uint64_t denominator = timebase.denom ? timebase.denom : 1; + uint64_t nowUs = mach_absolute_time() * (uint64_t)timebase.numer / + denominator / 1000ULL; + uint64_t sequence = ++state.sequence; + + /* Chromium 108 release BeginFrameArgs: frame/deadline/interval, + * BeginFrameId, trace id, type and critical-path flag. */ + unsigned char args[128] = {}; + *(uint64_t *)(args + 0) = nowUs; + *(uint64_t *)(args + 8) = nowUs + 16666ULL; + *(uint64_t *)(args + 16) = 16666ULL; + *(uint64_t *)(args + 24) = 0xffffffffULL; + *(uint64_t *)(args + 32) = sequence; + *(int64_t *)(args + 40) = -1; + *(uint32_t *)(args + 48) = 1; + args[52] = 1; + unsigned char details[24] = {}; + + /* Save the sequence before entering Chromium; OnBeginFrame can re-enter + * the needs callback and rewrite this same host record. */ + int fd = open(rendererBeginStatePath(), O_WRONLY | O_TRUNC, 0644); + if (fd >= 0) { + write(fd, &state, sizeof(state)); + close(fd); + } + ((void (*)(void *, void *, void *))(base + kRendererOnBeginVA))( + (void *)state.sink, args, details); + if (sequence <= 8) + frameSinkTrace("renderer-pump", sequence); +} + +static void startRendererBeginTimer(void) { + CFRunLoopTimerRef timer = CFRunLoopTimerCreate(NULL, + CFAbsoluteTimeGetCurrent() + 2.0, 1.0 / 60.0, 0, 0, + rendererBeginTimerF, NULL); + if (timer) + CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopDefaultMode); +} + +static void installFrameSinkBridge(void) { + /* Private Chromium callbacks cannot safely be invoked across Darling's + * divergent guest-memory views. Keep all experimental detours disabled. */ + return; + + for (unsigned i = 0; i < _dyld_image_count(); ++i) { + const char *name = _dyld_get_image_name(i); + if (!name || !strstr(name, "Versions/A/Electron Framework")) + continue; + unsigned char *base = (unsigned char *) _dyld_get_image_header(i); + bool nullGuardInstalled = installNullDetailsThunkGuard(base); + frameSinkTrace("null-guard", nullGuardInstalled ? 1UL : 0UL); + /* All earlier FrameSink experiments remain disabled. */ + return; + unsigned char *shouldSend = base + kCFSSShouldSendVA; + unsigned char shouldSendWant[] = { + 0x55, 0x48, 0x89, 0xe5, 0x41, 0x57 + }; + unsigned char shouldSendReplacement[] = { + 0xb8, 0x01, 0x00, 0x00, 0x00, 0xc3 + }; + /* Returning true keeps viz from treating a no-damage reply as a reason + * to stop the renderer's begin-frame stream. */ + frameSinkPatchBytes(shouldSend, shouldSendWant, sizeof(shouldSendWant), + shouldSendReplacement); + /* Do not detour live Chromium callback entries here. A 12-byte absolute + * jump cannot be installed atomically and raced BrowserWindow startup. + * The guarded ShouldSend patch above is complete before these callbacks + * are active and is the only safe FrameSink compatibility change. */ + bool directInstalled = false; + bool bundleInstalled = false; + bool rendererNeedsInstalled = false; + frameSinkTrace("install", (directInstalled ? 1UL : 0UL) | + (bundleInstalled ? 2UL : 0UL) | + (rendererNeedsInstalled ? 4UL : 0UL)); + /* A second CoreVideo image may run this constructor after the first + * image has already patched Chromium. Its local hook structs are empty; + * preserve the first image's host-file prologue in that case. */ + if (!frameSinkStateIsUsable() && directInstalled && bundleInstalled) { + FrameSinkSavedState saved = {}; + saved.magic = kFrameSinkSavedMagic; + memcpy(saved.direct, gDirectClientHook.saved, sizeof(saved.direct)); + memcpy(saved.bundle, gBundleClientHook.saved, sizeof(saved.bundle)); + int fd = open(frameSinkStatePath(), O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd >= 0) { + if (flock(fd, LOCK_EX) == 0) { + write(fd, &saved, sizeof(saved)); + flock(fd, LOCK_UN); + } + close(fd); + } + } + if (!rendererHookStateIsUsable() && rendererNeedsInstalled) { + RendererHookSavedState saved = {}; + saved.magic = kRendererHookMagic; + memcpy(saved.saved, gRendererNeedsHook.saved, sizeof(saved.saved)); + int fd = open(rendererHookStatePath(), + O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd >= 0) { + write(fd, &saved, sizeof(saved)); + close(fd); + } + } + if (rendererNeedsInstalled) + startRendererBeginTimer(); + return; + } +} + +__attribute__((constructor)) static void installFrameSinkBridgeAtLoad(void) { + installFrameSinkBridge(); +} + static const NSString* kDirectDisplayArray = @"CGDirectDisplay"; +/* + * darling-lol M58b final pump design: host-file-backed display-link state. + * + * Diagnosis this session: under Darling, guest helper threads (dispatch + * sources, detached pthreads, CFRunLoop timers) do not reliably observe + * writes made elsewhere in the guest — globals, dictionary contents and + * even objc associated-object storage diverge for the timer's thread, and + * a detached pthread sleep loop hangs inside its FIRST sleep. The one + * store proven to round-trip across processes AND across the shim's two + * loaded instances is the HOST filesystem (/dev/shm via shm_open, used by + * the M58b IOSurface work, and fileports via the M58 kobject machinery). + * + * So the shim keeps NO guest-side state. The display link's state lives in + * a per-pid host file; Create/Start/SetOutputCallback/Stop are trivial side + * effects on that file; the runloop timer fires every 1/60 s and reads the + * file fresh each tick, invoking Chromium's callback with a plausible + * CVTimeStamp. The timer itself fires (verified: continuous ~60 Hz records). + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#import +#import +#import +#import +#include +#include + + +/* sturdy host-shared record, written fresh on every mutation. */ +struct CVLinkStateRec { + uint32_t magic; /* 0xC81DFA10 */ + uint32_t version; + void* callback; + void* userInfo; + uint32_t running; + uint32_t alive; + uint32_t ticks; + uint32_t nullTicks; + /* darling-lol M58e: number of CVDisplayLinkRef objects alive in this + * process. The pump must keep ticking while >= 1 link exists, and MUST + * stop invoking the callback after the last link is released — the + * callback targets Chromium's DisplayLinkMac, which unregisters its + * callback by releasing the link (calling it afterwards would be + * use-after-free every tick). */ + uint32_t refs; + uint64_t reserved; + CVDisplayLinkRef link; +}; +static const uint32_t kStateMagic = 0xC81DFA10u; +static const uint32_t kStateVersion = 1; + +static void traceLog(const char* fmt, uint64_t a, uint64_t b) { + if (!getenv("RCLOL_CV_TRACE")) return; + char msg[192]; + struct timespec ts; +#ifdef CLOCK_MONOTONIC + clock_gettime(CLOCK_MONOTONIC, &ts); +#else + ts.tv_sec = (time_t) mach_absolute_time() / 1000000000ull; + ts.tv_nsec = 0; +#endif + snprintf(msg, sizeof(msg), "%lld.%03ld %d %s", + (long long) ts.tv_sec, ts.tv_nsec / 1000000, + (int) getpid(), /* msg below */ ""); + snprintf(msg + strlen(msg), sizeof(msg) - strlen(msg), fmt, a, b); + int fd = open("/Volumes/SystemRoot/tmp/opencode/cvpump.log", + 1 | 0x200 | 8, 0644); + if (fd >= 0) { write(fd, msg, strlen(msg)); close(fd); } +} + +static const char* statePathFor(void) { + static char path[128]; + static bool init = false; + if (!init) { + snprintf(path, sizeof(path), "/dev/shm/darling-cvlink-%d.state", + (int) getpid()); + init = true; + } + return path; +} + +/* These private records live in the host /dev/shm. Bypass recursive guest + * path expansion on Chromium's small callback stacks, and avoid frame-time RPC. */ +extern "C" long __lolsys_syscall(long,long,long,long,long,long,int); +static int clockOpen(void) { return (int)__lolsys_syscall((long)statePathFor(),66,0600,0,0,0,2); } +static void clockClose(int fd) { __lolsys_syscall(fd,0,0,0,0,0,3); } +static long clockRead(int fd,void *p,size_t n,off_t offset) { return __lolsys_syscall(fd,(long)p,n,offset,0,0,17); } +static long clockWrite(int fd,const void *p,size_t n,off_t offset) { return __lolsys_syscall(fd,(long)p,n,offset,0,0,18); } +static int clockFlock(int fd,int op) { return (int)__lolsys_syscall(fd,op,0,0,0,0,73); } +static bool readState(CVLinkStateRec *out) { + int fd=clockOpen();if(fd<0)return false; + long got=clockRead(fd,out,sizeof(*out),0);clockClose(fd); + return got==sizeof(*out) && out->magic==kStateMagic && out->version>=1 && out->alive; +} +static bool writeState(CVLinkStateRec *rec) { + int fd=clockOpen();if(fd<0)return false; + long put=clockWrite(fd,rec,sizeof(*rec),0);clockClose(fd);return put==sizeof(*rec); +} +struct CVStateGuard { + int fd; bool locked; + CVStateGuard() : fd(clockOpen()), locked(false) { + if(fd<0)return; + uint64_t owner=0;clockRead(fd,&owner,sizeof(owner),offsetof(CVLinkStateRec,reserved)); + uint64_t thread=(uintptr_t)pthread_self(); + if(!owner || owner!=thread)locked=clockFlock(fd,LOCK_EX)==0; + } + ~CVStateGuard(){if(fd>=0){if(locked)clockFlock(fd,LOCK_UN);clockClose(fd);}} +}; +__attribute__((constructor)) static void initializeDisplayClockRecord(void) { + CVLinkStateRec rec={};rec.magic=kStateMagic;rec.version=kStateVersion;rec.alive=1;writeState(&rec); +} +static void registerDisplayLink(CVDisplayLinkRef link) { + CVStateGuard guard;CVLinkStateRec rec={}; + if(readState(&rec)){++rec.refs;if(!rec.callback)rec.link=link;writeState(&rec);} +} + +static const char* displayLinkTick = + "displayLinkTick"; /* marker so grep can find the tick body */ + +static void displayLinkTickFromRec(CVLinkStateRec* rec) { + CVDisplayLinkOutputCallback callback = (CVDisplayLinkOutputCallback) rec->callback; + if (traceLog != NULL) { + /* darling-lol M58d: full continuity logging while the begin-frame + * stall is under diagnosis (previously capped at 32 records). */ + traceLog("PUMP cb=%p ticks=%llu\n", (uint64_t) callback, rec->ticks); + } + if (callback != NULL && rec->running && rec->refs) { + rec->ticks++; + struct mach_timebase_info timebase; + mach_timebase_info(&timebase); + uint64_t periodNs = 1000000000ull / 60ull; + uint64_t periodMach = periodNs * timebase.denom / timebase.numer; + + uint64_t hostTime = mach_absolute_time(); + CVTimeStamp now = {}; + now.version = 0; + now.hostTime = hostTime; + now.videoTime = hostTime / periodMach; + now.videoTimeScale = 60; + now.videoRefreshPeriod = 1; + now.flags = kCVTimeStampHostTimeValid | kCVTimeStampVideoTimeValid | + kCVTimeStampVideoRefreshPeriodValid; + CVTimeStamp outputTime = now; + outputTime.hostTime = hostTime + periodMach; + outputTime.videoTime = now.videoTime + 1; + CVOptionFlags flagsOut = 0; + callback(rec->link, &now, &outputTime, 0, &flagsOut, rec->userInfo); + } else { + rec->nullTicks++; + } +} + +/* The runloop timer's handler: re-read the state file fresh each tick. */ +static void DisplayLinkMainTimerF(CFRunLoopTimerRef timer, void* info) { + CVLinkStateRec rec; + if (!readState(&rec)) + return; + displayLinkTickFromRec(&rec); + writeState(&rec); +} + +/* darling-lol M58d: dispatch-source pump. + * + * Diagnosis this session: the runloop-timer pump only ever fires in the + * browser process (12/s measured) — the GPU process has NO CFRunLoop default + * mode traffic: PopupMenu/ChatWindow wait, Chromium's GPU-process message + * loop does not service the Cocoa default runloop constantly, so the CFRunLoop + * timer never fires there and viz *never sends a single begin frame* — the + * compositor surfaces are created but never drawn (a 1-publish artwork per + * run collapses). Per the M58b finding, dispatch sources DO fire from any + * thread; the handler just couldn't see guest memory. This shim already keeps + * its state in a HOST FILE (/dev/shm/darling-cvlink-.state) — the same + * store that survived everything else — so a dispatch-source timer can safely + * re-read it and invoke the callback. + */ +static void dispatchTimerFire(void* /*info*/) { + CVStateGuard guard;CVLinkStateRec rec; + if (guard.fd<0 || !readState(&rec)) return; + uint64_t owner=(uintptr_t)pthread_self(); + clockWrite(guard.fd,&owner,sizeof(owner),offsetof(CVLinkStateRec,reserved)); + displayLinkTickFromRec(&rec); + /* Preserve concurrent/reentrant registration changes; update only counters. */ + clockWrite(guard.fd,&rec.ticks,sizeof(rec.ticks)+sizeof(rec.nullTicks),offsetof(CVLinkStateRec,ticks)); + owner=0;clockWrite(guard.fd,&owner,sizeof(owner),offsetof(CVLinkStateRec,reserved)); +} + +/* Keep callbacks off Darling's dispatch trampoline: Chromium can leave its + * emulated workqueue with less than a page of usable stack. */ +static void *displayClockThread(void *) { + struct { long sec, nsec; } deadline; + if (__lolsys_syscall(1,(long)&deadline,0,0,0,0,228)<0) return NULL; + static uint64_t cvIntervalNS = 0; + if (!cvIntervalNS) { + const char *hz = getenv("RCLOL_REFRESH_HZ"); + int rate = hz ? atoi(hz) : 0; + cvIntervalNS = (rate > 0 && rate <= 1000) ? 1000000000ull / (uint64_t)rate : (1000000000ull / 60ull); + } + for (;;) { + deadline.nsec += cvIntervalNS; + if(deadline.nsec>=NSEC_PER_SEC){deadline.nsec-=NSEC_PER_SEC;++deadline.sec;} + long result; + do { result=__lolsys_syscall(1,1,(long)&deadline,0,0,0,230); } while(result==-4); + if(result<0) return NULL; + dispatchTimerFire(NULL); + struct { long sec, nsec; } now; + if(__lolsys_syscall(1,(long)&now,0,0,0,0,228)==0) { + long long diffNS = ((long long)now.sec - (long long)deadline.sec) * 1000000000LL + (now.nsec - deadline.nsec); + if (diffNS > (long long)cvIntervalNS) { + deadline.sec = now.sec; + deadline.nsec = now.nsec; + } + } + } +} +static void InstallDispatchPump(void) { + pthread_attr_t attr;pthread_attr_init(&attr); + pthread_attr_setstacksize(&attr,1024*1024); + pthread_t thread; + if(pthread_create(&thread,&attr,displayClockThread,NULL)==0)pthread_detach(thread); + pthread_attr_destroy(&attr); +} + CVReturn CVDisplayLinkCreateWithActiveCGDisplays(CVDisplayLinkRef* displayLinkOut) { - uint32_t displayCount; + uint32_t displayCount = 0; std::unique_ptr displays; + /* darling-lol M57: tolerate a missing/incomplete display list instead of + * failing (Chromium dereferences a null link when this errors). */ CGError err = CGGetActiveDisplayList(0, nullptr, &displayCount); if (err != kCGErrorSuccess) - return err; + displayCount = 0; - displays.reset(new CGDirectDisplayID[displayCount]); + if (displayCount > 0) { + displays.reset(new CGDirectDisplayID[displayCount]); + err = CGGetActiveDisplayList(displayCount, displays.get(), &displayCount); + if (err != kCGErrorSuccess) + displayCount = 0; + } - err = CGGetActiveDisplayList(displayCount, displays.get(), &displayCount); - if (err != kCGErrorSuccess) - return err; - NSMutableDictionary* self = [[NSMutableDictionary alloc] init]; NSMutableArray* array = [NSMutableArray arrayWithCapacity: displayCount]; - for (int i = 0; i < displayCount; i++) + for (uint32_t i = 0; i < displayCount; i++) [array addObject: [NSNumber numberWithInt: displays[i]]]; + /* darling-lol M57: ensure at least one display id so + * CVDisplayLinkGetCurrentCGDisplay() is non-zero (Chromium's + * DisplayLinkMac::GetForDisplay rejects a zero current display). */ + if ([array count] == 0) + [array addObject: [NSNumber numberWithInt: 1]]; + [self setObject: array forKey: kDirectDisplayArray]; + registerDisplayLink((CVDisplayLinkRef)self); *displayLinkOut = (CVDisplayLinkRef) self; return kCVReturnSuccess; } @@ -41,35 +890,128 @@ CVReturn CVDisplayLinkStart(CVDisplayLinkRef displayLink) { if (!displayLink) return kCVReturnInvalidArgument; - // TODO + CVStateGuard guard; + + /* Install in every process that loads CoreVideo. The renderer may not + * create a CVDisplayLink of its own, so CVDisplayLinkStart is too late to + * be the only installation point. */ + installFrameSinkBridge(); + + /* darling-lol M58b: mark the shared state file running; the runloop + * timer is installed once per process (the guard `started` uses a + * static flag rather than per-link state, since state lives in the + * file, not in guest memory). */ + static bool started = false; + if (!started) { + CVLinkStateRec rec; + if(!readState(&rec)) memset(&rec,0,sizeof(rec)); + rec.magic = kStateMagic; + rec.version = kStateVersion; + rec.alive = 1; + rec.running = 1; + writeState(&rec); + + /* darling-lol M58d: dispatch-source pump, one per process. + * The old CFRunLoop timer only serviced the browser's main + * runloop (~12/s) and NEVER fired in the GPU process (no begin + * frames from viz), so frame production starved + * nondeterministically. Dispatch sources fire from any thread + * and this shim's state lives in a host file, which is exactly + * the store proven to cross the helper-thread visibility gap. */ + InstallDispatchPump(); + started = true; + } else { + CVLinkStateRec rec; + if (readState(&rec)) { + rec.running = 1; + rec.alive = 1; + writeState(&rec); + } + } return kCVReturnSuccess; } CVReturn CVDisplayLinkStop(CVDisplayLinkRef displayLink) { - if (!displayLink) - return kCVReturnInvalidArgument; - // TODO - return kCVReturnSuccess; + if(!displayLink)return kCVReturnInvalidArgument; + CVStateGuard guard;CVLinkStateRec rec; + if(readState(&rec)) { rec.running=0;writeState(&rec); } + return kCVReturnSuccess; } Boolean CVDisplayLinkIsRunning(CVDisplayLinkRef displayLink) { - if (!displayLink) + if(!displayLink)return false; + CVLinkStateRec rec; + if (!readState(&rec)) return false; - // TODO - return true; + return rec.running != 0; +} + +CVDisplayLinkRef CVDisplayLinkRetain(CVDisplayLinkRef displayLink) +{ + /* darling-lol M57: Chromium's DisplayLinkMac retains/releases the link. */ + if (displayLink) { + CVStateGuard guard; + [(NSMutableDictionary*) displayLink retain]; + /* darling-lol M58e: bump the shared refs count so the pump keeps + * firing while any link exists. */ + CVLinkStateRec rec; + if (readState(&rec)) { + rec.refs++; + rec.alive = 1; + writeState(&rec); + } + } + return displayLink; } void CVDisplayLinkRelease(CVDisplayLinkRef displayLink) { - NSMutableDictionary* self = (NSMutableDictionary*) displayLink; - [self release]; + if (!displayLink) + return; + CVStateGuard guard; + /* darling-lol M58e: drop the shared refs count. When the last link of + * this process goes away, drop the callback registration so the leaked + * pump timer stops invoking a freed Chromium callback (use-after-free + * guard). */ + CVLinkStateRec rec; + if (readState(&rec)) { + if (rec.refs > 0) + rec.refs--; + if (rec.refs == 0 || (rec.link==displayLink && [(NSMutableDictionary*)displayLink retainCount]==1)) { + rec.callback = NULL; + rec.userInfo = NULL; + rec.link=NULL; + } + writeState(&rec); + } + [(NSMutableDictionary*) displayLink release]; } +/* Creation, retain and release maintain the live link count. Stop cancels + * callbacks; callback-side Stop is reentrant, while outside Stop/Release wait + * for the active callback before clearing or destroying its registration. */ + CVReturn CVDisplayLinkSetOutputCallback(CVDisplayLinkRef displayLink, CVDisplayLinkOutputCallback callback, void *userInfo) { - // TODO + if (!displayLink) + return kCVReturnInvalidArgument; + + CVStateGuard guard; + CVLinkStateRec rec; + if (!readState(&rec)) { + memset(&rec, 0, sizeof(rec)); + rec.magic = kStateMagic; + rec.version = kStateVersion; + rec.refs = 1; + rec.alive = 1; + } + rec.link=displayLink; + rec.callback = (void*) callback; + rec.userInfo = userInfo; + writeState(&rec); + traceLog("SETOUT cb=%p pid_ok\n", (uint64_t) callback, 0); return kCVReturnSuccess; } @@ -91,7 +1033,7 @@ CVReturn CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(CVDisplayLinkRef disp CGDirectDisplayID displayID = [window.screen cgDirectDisplayID]; NSMutableDictionary* self = (NSMutableDictionary*) displayLink; - [self setObject: @[[NSNumber numberWithInt: displayID]] + [self setObject: @[[NSNumber numberWithInt: (int)(displayID != 0 ? displayID : 1)]] forKey: kDirectDisplayArray]; return kCVReturnSuccess; } @@ -117,14 +1059,31 @@ CVReturn CVDisplayLinkCreateWithCGDisplay( CGDirectDisplayID displayID, CV_RETURNS_RETAINED_PARAMETER CVDisplayLinkRef CV_NULLABLE * CV_NONNULL displayLinkOut ) { - if (displayID == kCGNullDirectDisplay) + /* darling-lol M57: Chromium's NativeWidgetMacNSWindowHost aborts (and then + * crashes on the null link) when this returns non-success; Darling's + * CGDirectDisplay ids are unreliable. Always hand back a valid object, and + * make sure the current display is non-zero (DisplayLinkMac::GetForDisplay + * rejects a zero current display). */ + if (displayLinkOut == nullptr) return kCVReturnInvalidArgument; NSMutableDictionary* self = [[NSMutableDictionary alloc] init]; - [self setObject: @[ [NSNumber numberWithInt: displayID] ] - forKey: @"CGDirectDisplay"]; + [self setObject: @[ [NSNumber numberWithInt: (int)(displayID != 0 ? displayID : 1) ] ] + forKey: kDirectDisplayArray]; + /* Optional creation diagnostics. Creation leaves the link stopped. */ + if (getenv("RCLOL_CV_TRACE")) { + int fd = open("/Volumes/SystemRoot/tmp/opencode/cvpump.log", 1 | 0x200 | 8, 0644); + if (fd >= 0) { + char msg[64]; + snprintf(msg, sizeof(msg), "CREATE_WITH_CG_DISPLAY id=%d\n", (int)(displayID)); + write(fd, msg, strlen(msg)); + close(fd); + } + } + registerDisplayLink((CVDisplayLinkRef)self); *displayLinkOut = (CVDisplayLinkRef) self; + return kCVReturnSuccess; } diff --git a/src/frameworks/CoreVideo/src/constants.c b/src/frameworks/CoreVideo/src/constants.c index 02f06704e..5d4fd4b2d 100644 --- a/src/frameworks/CoreVideo/src/constants.c +++ b/src/frameworks/CoreVideo/src/constants.c @@ -42,6 +42,11 @@ const CFStringRef kCVImageBufferTransferFunction_Linear = CFSTR("Linear"); const CFStringRef kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ = CFSTR("SMPTE_ST_2084_PQ"); const CFStringRef kCVImageBufferTransferFunction_SMPTE_ST_428_1 = CFSTR("SMPTE_ST_428_1"); const CFStringRef kCVImageBufferTransferFunction_SMPTE_C = CFSTR("SMPTE_C"); +const CFStringRef kCVImageBufferTransferFunction_sRGB = CFSTR("sRGB"); +const CFStringRef kCVBufferPropagatedAttachmentsKey = CFSTR("CVBufferPropagatedAttachments"); +const CFStringRef kCVImageBufferContentLightLevelInfoKey = CFSTR("ContentLightLevelInfo"); +const CFStringRef kCVImageBufferMasteringDisplayColorVolumeKey = CFSTR("MasteringDisplayColorVolume"); +const CFStringRef kCVPixelBufferPoolAllocationThresholdKey = CFSTR("AllocationThreshold"); const CFStringRef kCVPixelBufferIOSurfacePropertiesKey = CFSTR("IOSurfaceProperties"); const CFStringRef kCVPixelBufferOpenGLCompatibilityKey = CFSTR("OpenGLCompatibility"); diff --git a/src/frameworks/CryptoTokenKit/CMakeLists.txt b/src/frameworks/CryptoTokenKit/CMakeLists.txt index 6260e9b35..c02096039 100644 --- a/src/frameworks/CryptoTokenKit/CMakeLists.txt +++ b/src/frameworks/CryptoTokenKit/CMakeLists.txt @@ -30,8 +30,10 @@ add_framework(CryptoTokenKit src/ctkclient.c src/ctkclient_test.c src/ctkloginhelper.c + src/TKTokenWatcher.m DEPENDENCIES system CoreFoundation + Foundation ) diff --git a/src/frameworks/CryptoTokenKit/include/CryptoTokenKit/TKTokenWatcher.h b/src/frameworks/CryptoTokenKit/include/CryptoTokenKit/TKTokenWatcher.h index d7ad1ed23..02230985d 100755 --- a/src/frameworks/CryptoTokenKit/include/CryptoTokenKit/TKTokenWatcher.h +++ b/src/frameworks/CryptoTokenKit/include/CryptoTokenKit/TKTokenWatcher.h @@ -2,6 +2,10 @@ Image: /System/Library/PrivateFrameworks/CryptoTokenKit.framework/CryptoTokenKit */ +#import + +@protocol TKProtocolTokenWatcher; + @interface TKTokenWatcher : NSObject { NSXPCConnection * _connection; id /* block */ _insertionHandler; @@ -12,7 +16,6 @@ @property (readonly) NSArray *tokenIDs; -- (void).cxx_destruct; - (void)addRemovalHandler:(id /* block */)arg1 forTokenID:(id)arg2; - (void)dealloc; - (id)init; diff --git a/src/frameworks/DiskArbitration/DADisk.c b/src/frameworks/DiskArbitration/DADisk.c index 83f1793b8..acc09c088 100644 --- a/src/frameworks/DiskArbitration/DADisk.c +++ b/src/frameworks/DiskArbitration/DADisk.c @@ -82,6 +82,41 @@ DADiskInitialize (void) _kDADiskTypeID = _CFRuntimeRegisterClass (&DADiskClass); } +CFArrayRef kDADiskDescriptionWatchVolumePath = NULL; +CFArrayRef kDADiskDescriptionWatchVolumeName = NULL; +CFDictionaryRef kDADiskDescriptionMatchMediaWhole = NULL; +CFDictionaryRef kDADiskDescriptionMatchMediaUnformatted = NULL; +CFDictionaryRef kDADiskDescriptionMatchVolumeMountable = NULL; +CFDictionaryRef kDADiskDescriptionMatchVolumeUnrecognized = NULL; + +__attribute__((constructor)) +static void +DADiskInitializeWatchArrays (void) +{ + const void *pathKeys[] = { kDADiskDescriptionVolumePathKey }; + const void *nameKeys[] = { kDADiskDescriptionVolumeNameKey }; + kDADiskDescriptionWatchVolumePath = CFArrayCreate(NULL, pathKeys, 1, + &kCFTypeArrayCallBacks); + kDADiskDescriptionWatchVolumeName = CFArrayCreate(NULL, nameKeys, 1, + &kCFTypeArrayCallBacks); + kDADiskDescriptionMatchMediaWhole = CFDictionaryCreate(NULL, + (const void **)&kDADiskDescriptionMediaWholeKey, + (const void **)&kCFBooleanTrue, 1, + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + kDADiskDescriptionMatchMediaUnformatted = CFDictionaryCreate(NULL, + (const void **)&kDADiskDescriptionMediaContentKey, + (const void **)&kCFBooleanTrue, 1, + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + kDADiskDescriptionMatchVolumeMountable = CFDictionaryCreate(NULL, + (const void **)&kDADiskDescriptionVolumeMountableKey, + (const void **)&kCFBooleanTrue, 1, + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + kDADiskDescriptionMatchVolumeUnrecognized = CFDictionaryCreate(NULL, + (const void **)&kDADiskDescriptionVolumeKindKey, + (const void **)&kCFBooleanTrue, 1, + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); +} + #define DADISK_SIZE sizeof(struct __DADisk) - sizeof(CFRuntimeBase) static void diff --git a/src/frameworks/IOSurface/include/IOSurface/IOSurfacePriv.h b/src/frameworks/IOSurface/include/IOSurface/IOSurfacePriv.h index d8aed37f1..dd6266cad 100644 --- a/src/frameworks/IOSurface/include/IOSurface/IOSurfacePriv.h +++ b/src/frameworks/IOSurface/include/IOSurface/IOSurfacePriv.h @@ -85,4 +85,43 @@ struct _IOSurfaceLockUnlock uint32_t options; }; +/* darling-lol M58b: cross-process identity for user-space IOSurfaces. + * + * Darling has no IOSurfaceRoot IOKit service, so IOSurfaces are user-space + * allocations. To let Chromium hand a compositor frame's surface between + * processes (GPU -> browser via a real Mach port right, required by + * ui/gfx/mac/io_surface.cc's mach_port_mod_refs checks), a fallback surface's + * backing is a host shared-memory file (/dev/shm with the dumbest portable + * call, shm_open) laid out as: + * + * offset 0 : struct _IOSurfaceWireHeader + * offset IOSURFACE_WIRE_DATA_OFFSET : plane 0 pixel data + * + * IOSurfaceCreateMachPort() wraps the backing fd in a fileport (see + * darlingserver fileport.c), producing a real Mach send right that rides a + * Mach message. IOSurfaceLookupFromMachPort() dups the fd back + * (fileport_makefd), maps it MAP_SHARED, and rebuilds the IOSurface object + * from the wire header. Processes share the *same memory*, so frame updates + * are live. */ + +#define IOSURFACE_WIRE_MAGIC 0x46535349u /* 'ISMF' */ +#define IOSURFACE_WIRE_VERSION 2u +#define IOSURFACE_WIRE_DATA_OFFSET 4096u + +struct _IOSurfaceWireHeader +{ + uint32_t magic; /* IOSURFACE_WIRE_MAGIC */ + uint32_t version; /* IOSURFACE_WIRE_VERSION */ + uint32_t surfaceID; + uint32_t pixelFormat; + uint32_t planeCount; + uint32_t reserved; + uint64_t memoryOffset; /* == IOSURFACE_WIRE_DATA_OFFSET */ + uint32_t width; + uint32_t height; + uint32_t bytesPerRow; + uint32_t bytesPerElement; + struct _IOSurfacePlane planes[4]; +}; + #endif diff --git a/src/frameworks/IOSurface/src/IOSurfaceObjC.m b/src/frameworks/IOSurface/src/IOSurfaceObjC.m index 7821ba7a4..951810c92 100644 --- a/src/frameworks/IOSurface/src/IOSurfaceObjC.m +++ b/src/frameworks/IOSurface/src/IOSurfaceObjC.m @@ -23,6 +23,20 @@ #import #import #include +#include + +/* darling-lol M58b: the guest libc's POSIX shm/mmap API (implemented by + * elfcalls against the host, so the allocation is really /dev/shm memory + * that another Darling process can map). */ +#include +#include +#include +#include +#include +#include +#include +#include +#include static io_service_t g_surfaceService; @@ -30,22 +44,69 @@ typedef struct { struct _IOSurfaceObjectRetval* surface; _Atomic int32_t localUseCount; + /* darling-lol M58b: when the fallback allocation is backed by a host + * shared-memory file (see IOSurfacePriv.h), these hold the mapping so + * dealloc can unmap/close instead of free()ing platform-managed memory. + * shm_fd < 0 means a plain calloc'd private allocation. */ + int shm_fd; + uint64_t shm_size; + mach_port_t shm_port; + NSMutableDictionary* attachments; } ImplData; +/* Surface IDs cross process boundaries and must not restart at one per process. */ +extern long __lolsys_syscall(long,long,long,long,long,long,int); +static IOSurfaceID allocateSurfaceID(void) +{ + char name[80]; + snprintf(name, sizeof(name), "/dev/shm/darling-iosurface-counter-%u", (unsigned)getuid()); + /* The counter is host shared memory, never a guest fileport. Native + * syscalls preserve the same cross-process flock without routing every + * video-frame allocation through Darling's descriptor/event machinery. */ + int fd = (int)__lolsys_syscall((long)name,66|02000000 /* Linux O_CLOEXEC */,0600,0,0,0,2); + if (fd < 0) return 0; + long locked; + do { locked=__lolsys_syscall(fd,LOCK_EX,0,0,0,0,73); } while(locked==-4); + if (locked != 0) { __lolsys_syscall(fd,0,0,0,0,0,3); return 0; } + IOSurfaceID value = 0; + long count; + do { count=__lolsys_syscall(fd,(long)&value,sizeof(value),0,0,0,17); } while(count==-4); + if ((count != 0 && count != sizeof(value)) || value == UINT32_MAX) { + __lolsys_syscall(fd,0,0,0,0,0,3); return 0; + } + ++value; + long written; + do { written=__lolsys_syscall(fd,(long)&value,sizeof(value),0,0,0,18); } while(written==-4); + if (written != sizeof(value)) value = 0; + __lolsys_syscall(fd,0,0,0,0,0,3); + return value; +} + @implementation IOSurface + (void)initialize { if (self == [IOSurface self]) { - CFMutableDictionaryRef matching = IOServiceNameMatching("IOSurfaceRoot"); - - g_surfaceService = IOServiceGetMatchingService(kIOMasterPortDefault, matching); - - CFRelease(matching); - - if (!g_surfaceService) - NSLog(@"Cannot obtain IOSurfaceRoot!\n"); + /* darling-lol M58e: do not touch IOKit here at all. + * + * This class initializer used to look the IOSurfaceRoot IOKit + * service up on the first IOSurface use of every Darling + * process. There is no way it can SUCCEED under Darling (no + * IOSurfaceRoot IOKit server exists — user-space fallbacks + * below already key everything off g_surfaceService == 0), but + * when the runtime's class-initialization machinery is unlucky + * (/helper processes racing CoreFoundation's first CF calls), + * the CF objects involved in the call chain can be reported + * with garbage isa pointers and the plain CFRelease of the + * matching dictionary SEGVs (measured: intermittent SIGSEGV at + * window startup in both browser and GPU processes, with LOLSIG + * chains ending in libsystem_malloc's free path and in + * objc_msgSend on a dead receiver — both reproducible ~1:3 runs + * before this change; everything symbolized through the M58e + * SIGFRAME walk). */ + g_surfaceService = 0; + NSLog(@"IOSurface: user-space route (no IOKit IOSurfaceRoot).\n"); } } @@ -56,6 +117,10 @@ typedef struct memcpy(idata->surface, bytes, length); idata->localUseCount = 0; + idata->shm_fd = -1; + idata->shm_size = 0; + idata->shm_port = MACH_PORT_NULL; + idata->attachments = [[NSMutableDictionary alloc] init]; self->_impl = idata; return self; @@ -63,6 +128,10 @@ typedef struct - (nullable instancetype)initWithProperties:(NSDictionary *)properties { + /* The fallback service is absent by construction. Avoid serializing and + * issuing an IOKit request that cannot succeed for every video frame. */ + if (g_surfaceService == 0) + return [self _initWithUserSpaceProperties: properties]; CFDataRef data = IOCFSerialize((CFTypeRef) properties, kIOCFSerializeToBinary); uint8_t responseBuffer[3500]; @@ -75,11 +144,183 @@ typedef struct if (ret != kIOSurfaceSuccess) { + /* darling-lol M57: Darling has no IOSurfaceRoot IOKit service, so the + * real create path always fails and Chromium's GPU process can't + * allocate any GPU memory buffer (it then exits with "drivers can't + * recover"). Fall back to a plain user-space allocation. */ + return [self _initWithUserSpaceProperties: (NSDictionary*) properties]; + } + + return [self initWithResponse: responseBuffer length: responseLength]; +} + +- (nullable instancetype)_initWithUserSpaceProperties:(NSDictionary *)properties +{ + NSNumber* widthNum = properties[(NSString*)kIOSurfaceWidth]; + NSNumber* heightNum = properties[(NSString*)kIOSurfaceHeight]; + NSNumber* bytesPerRowNum = properties[(NSString*)kIOSurfaceBytesPerRow]; + NSNumber* bytesPerElementNum = properties[(NSString*)kIOSurfaceBytesPerElement]; + NSNumber* pixelFormatNum = properties[(NSString*)kIOSurfacePixelFormat]; + + uint32_t width = widthNum ? widthNum.unsignedIntValue : 0; + uint32_t height = heightNum ? heightNum.unsignedIntValue : 0; + uint32_t bytesPerElement = bytesPerElementNum ? bytesPerElementNum.unsignedIntValue : 4; + if (bytesPerElement == 0) bytesPerElement = 4; + uint32_t bytesPerRow = bytesPerRowNum ? bytesPerRowNum.unsignedIntValue : (width * bytesPerElement); + if (bytesPerRow == 0) bytesPerRow = width * bytesPerElement; + uint32_t pixelFormat = pixelFormatNum ? pixelFormatNum.unsignedIntValue : 0; + NSArray *planeInfo = properties[(NSString*)kIOSurfacePlaneInfo]; + unsigned planeCount = planeInfo.count ? (unsigned)planeInfo.count : 1; + if (planeCount > 4) { [self release]; return nil; } + struct _IOSurfacePlane planes[4] = {{0}}; + uint64_t allocSize = 0; + for (unsigned i=0; iUINT32_MAX || ph>UINT32_MAX || pe>16 || + pr>UINT32_MAX || prSIZE_MAX/pr || po>SIZE_MAX-pr*ph || + (i && poallocSize) allocSize=requested.unsignedLongLongValue; + if (allocSize>SIZE_MAX-IOSURFACE_WIRE_DATA_OFFSET) { [self release]; return nil; } + + /* darling-lol M58b: back the surface with a host shared-memory file so + * another process can map the same memory through a fileport handed + * off by IOSurfaceCreateMachPort(). The file holds a wire header at + * offset 0 and plane 0's data at IOSURFACE_WIRE_DATA_OFFSET. */ + uint64_t shmSize = IOSURFACE_WIRE_DATA_OFFSET + allocSize; + + int shm_fd = -1; + void* address = NULL; + char shmName[128]; + static _Atomic unsigned shmCounter = 0; + + shmName[0] = '\0'; + { + _Atomic unsigned counter = ++shmCounter; + unsigned attempt; + for (attempt = 0; attempt < 16; ++attempt) + { + /* O_EXCL to guarantee a unique /dev/shm object. */ + snprintf(shmName, sizeof(shmName), "/IOSurface-%d-%u-%u", + getpid(), (unsigned) counter + attempt, 0u); + shm_fd = shm_open(shmName, O_RDWR | O_CREAT | O_EXCL, 0600); + if (shm_fd >= 0) + break; + /* EEXIST -> next name; any other error releases the loop + * via the post-check below. */ + shm_fd = -1; + } + } + + if (shm_fd >= 0) + { + if (shm_unlink(shmName) != 0) + { + /* Nobody else needs to find this by name; unlink ASAP so + * the object dies with the last fd. */ + } + + if (ftruncate(shm_fd, (off_t) shmSize) != 0) + { + close(shm_fd); + shm_fd = -1; + } else { + address = mmap(NULL, shmSize, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); + if (address == NULL || address == MAP_FAILED) + { + close(shm_fd); + shm_fd = -1; + address = NULL; + } + } + } + + if (shm_fd < 0) + { + /* No shared memory available: plain private allocation (in-process + * only; IOSurfaceCreateMachPort has nothing to hand off). */ + shmSize = allocSize; + address = calloc(1, (size_t) allocSize); + if (address == NULL) + { + [self release]; + return nil; + } + } + + size_t structSize = sizeof(struct _IOSurfaceObjectRetval) + (planeCount-1)*sizeof(struct _IOSurfacePlane); + struct _IOSurfaceObjectRetval* surface = calloc(1, structSize); + if (surface == NULL) + { + if (shm_fd >= 0) + { + munmap(address, shmSize); + close(shm_fd); + } else { + free(address); + } [self release]; return nil; } - return [self initWithResponse: responseBuffer length: responseLength]; + surface->surfaceID = allocateSurfaceID(); + if (!surface->surfaceID) { + if (shm_fd >= 0) { munmap(address, shmSize); close(shm_fd); } + else free(address); + free(surface); + [self release]; + return nil; + } + surface->pixelFormat = pixelFormat; + surface->address = (uint64_t) address; + surface->planeCount = planeCount; + for (unsigned i=0; iplanes[i]=planes[i]; + if (shm_fd>=0) surface->planes[i].memoryOffset+=IOSURFACE_WIRE_DATA_OFFSET; + } + + if (shm_fd >= 0) + { + /* Fill in the wire header so IOSurfaceLookupFromMachPort() in + * another process can rebuild this object from the mapping. */ + struct _IOSurfaceWireHeader wire; + memset(&wire, 0, sizeof(wire)); + wire.magic = IOSURFACE_WIRE_MAGIC; + wire.version = IOSURFACE_WIRE_VERSION; + wire.surfaceID = surface->surfaceID; + wire.pixelFormat = surface->pixelFormat; + wire.planeCount = surface->planeCount; + wire.reserved = 0; + wire.memoryOffset = surface->planes[0].memoryOffset; + wire.width = surface->planes[0].width; + wire.height = surface->planes[0].height; + wire.bytesPerRow = surface->planes[0].bytesPerRow; + wire.bytesPerElement = surface->planes[0].bytesPerElement; + memcpy(wire.planes, surface->planes, planeCount*sizeof(struct _IOSurfacePlane)); + (void) pwrite(shm_fd, &wire, sizeof(wire), 0); + } + + ImplData* idata = (ImplData*) malloc(sizeof(ImplData)); + idata->surface = surface; + idata->localUseCount = 0; + idata->shm_fd = shm_fd; + idata->shm_size = shmSize; + idata->shm_port = MACH_PORT_NULL; + idata->attachments = [[NSMutableDictionary alloc] init]; + self->_impl = idata; + + return self; } - (nullable instancetype)initWithSurfaceID:(IOSurfaceID)surfaceID @@ -88,10 +329,19 @@ typedef struct size_t responseLength = sizeof(responseBuffer); uint64_t scalar = surfaceID; - kern_return_t ret = IOConnectCallMethod(g_surfaceService, kIOSurfaceMethodLookupByID, &scalar, 1, NULL, 0, NULL, 0, responseBuffer, &responseLength); + kern_return_t ret = g_surfaceService != 0 + ? IOConnectCallMethod(g_surfaceService, kIOSurfaceMethodLookupByID, &scalar, 1, NULL, 0, NULL, 0, responseBuffer, &responseLength) + : KERN_FAILURE; if (ret != kIOSurfaceSuccess) { + /* darling-lol: without the IOSurfaceRoot service (Darling) the + * lookup always fails, so re-resolve the ID against our own + * user-space registry instead of returning nil. */ + IOSurfaceRef surface = IOSurfaceLookup(surfaceID); + if (surface != NULL) + return (IOSurface*) surface; + [self release]; return nil; } @@ -99,18 +349,74 @@ typedef struct return [self initWithResponse: responseBuffer length: responseLength]; } +/* darling-lol: IOSurfaceLookupFromMachPort() calls this, but it was never + * implemented, so every cross-process frame handoff returned a half-built + * object. Recover a registered surface by token, else treat the token as an + * IOSurfaceID and try the registry through iosurface_lookup(). */ +- (nullable instancetype)initWithMachPort:(mach_port_t)port +{ + if (port == MACH_PORT_NULL) + { + [self release]; + return nil; + } + + IOSurfaceRef known = IOSurfaceLookup((IOSurfaceID) port); + if (known != NULL) + { + [self release]; + return (IOSurface*) known; + } + + /* Unknown token: build an empty placeholder rather than nil so callers + * that only need the object identity keep working. */ + return [self initWithResponse: "\0" length: 1]; +} + +/* Serialize final release with registry lookup/retain. The recursive lock + * permits dealloc to remove the entry while super's release is active. */ +- (oneway void) release +{ + extern void _IOSurfaceRegistryLock(void), _IOSurfaceRegistryUnlock(void); + _IOSurfaceRegistryLock(); + [super release]; + _IOSurfaceRegistryUnlock(); +} + - (void) dealloc { + extern void _IOSurfaceRegistryForget(IOSurfaceRef); + if (_impl) _IOSurfaceRegistryForget((IOSurfaceRef)self); ImplData* data = (ImplData*) _impl; if (data != NULL) { if (data->surface != NULL) { - IOConnectCallMethod(g_surfaceService, kIOSurfaceMethodRelease, NULL, 0, &data->surface->surfaceID, 1, NULL, 0, NULL, 0); + if (g_surfaceService != 0) + IOConnectCallMethod(g_surfaceService, kIOSurfaceMethodRelease, NULL, 0, &data->surface->surfaceID, 1, NULL, 0, NULL, 0); + + /* darling-lol M58b: free the surface's memory according to + * its backing; shared mappings belong to mmap, not the + * general allocator. */ + if (data->shm_fd >= 0) + { + if (data->surface->address != 0) + munmap((void*)(uintptr_t) data->surface->address, data->shm_size); + close(data->shm_fd); + } else if (data->surface->address != 0) { + free((void*)(uintptr_t) data->surface->address); + } + + /* The fileport right created by IOSurfaceCreateMachPort() + * keeps the backing alive for whoever it was handed to; + * releasing our send right is enough here. */ + if (data->shm_port != MACH_PORT_NULL) + mach_port_deallocate(mach_task_self(), data->shm_port); free(data->surface); } + [data->attachments release]; free(data); } [super dealloc]; @@ -236,11 +542,56 @@ typedef struct return (void*)(data->surface->address + data->surface->planes[planeIndex].memoryOffset); } +- (void)setAttachment:(id)anObject forKey:(NSString *)key +{ + if (!key) + return; + ImplData* data = (ImplData*) _impl; + if (anObject) + [data->attachments setObject:anObject forKey:key]; + else + [data->attachments removeObjectForKey:key]; +} + +- (id)attachmentForKey:(NSString *)key +{ + ImplData* data = (ImplData*) _impl; + return [data->attachments objectForKey:key]; +} + +- (void)removeAttachmentForKey:(NSString *)key +{ + ImplData* data = (ImplData*) _impl; + [data->attachments removeObjectForKey:key]; +} + +- (void)setAllAttachments:(NSDictionary *)dict +{ + ImplData* data = (ImplData*) _impl; + [data->attachments removeAllObjects]; + if (dict) + [data->attachments addEntriesFromDictionary:dict]; +} + +- (NSDictionary *)allAttachments +{ + ImplData* data = (ImplData*) _impl; + return data->attachments; +} + +- (void)removeAllAttachments +{ + ImplData* data = (ImplData*) _impl; + [data->attachments removeAllObjects]; +} + - (void)incrementUseCount { ImplData* data = (ImplData*) _impl; if ((data->localUseCount++) == 0) { + if (g_surfaceService == 0) + return; uint64_t scalar = data->surface->surfaceID; IOConnectCallMethod(g_surfaceService, kIOSurfaceMethodIncrementUseCount, &scalar, 1, NULL, 0, NULL, 0, NULL, 0); } @@ -251,6 +602,8 @@ typedef struct ImplData* data = (ImplData*) _impl; if (--data->localUseCount == 0) { + if (g_surfaceService == 0) + return; uint64_t scalar = data->surface->surfaceID; IOConnectCallMethod(g_surfaceService, kIOSurfaceMethodDecrementUseCount, &scalar, 1, NULL, 0, NULL, 0, NULL, 0); } @@ -262,6 +615,188 @@ typedef struct return data->localUseCount; } +/* darling-lol M58b: cross-process identity. IOSurfaceCreateMachPort() + * (in api.m) calls this to wrap this surface's shared backing in a fileport + * -- a REAL mach task-port send right referencing our /dev/shm file, so + * callers that do mach_port_mod_refs()/port-transfer checks get a valid + * name. Cached on the object so repeated creates reuse the same right. */ +static mach_port_t copySurfaceSendRight(mach_port_t port) +{ + /* The surface owns the cached reference; every Create caller owns another. */ + if (mach_port_mod_refs(mach_task_self(), port, MACH_PORT_RIGHT_SEND, 1) != KERN_SUCCESS) + return MACH_PORT_NULL; + return port; +} + +mach_port_t _IOSurfaceObjCShmFileportForSurface(IOSurfaceRef buffer) +{ + IOSurface* self = (IOSurface*) buffer; + ImplData* data = (ImplData*) self->_impl; + + if (buffer == NULL) + return MACH_PORT_NULL; + + data = (ImplData*) self->_impl; + if (data == NULL) + return MACH_PORT_NULL; + + /* darling-lol M58cz: reuse any right we already handed out. This also + * covers surfaces restored via initWithMachPort: (which have no shm_fd + * but may carry a port). */ + if (data->shm_port != MACH_PORT_NULL) + return copySurfaceSendRight(data->shm_port); + + if (data->shm_fd >= 0) + { + mach_port_t port = MACH_PORT_NULL; + if (fileport_makeport(data->shm_fd, &port) != KERN_SUCCESS || port == MACH_PORT_NULL) + { + NSLog(@"_IOSurfaceObjCShmFileportForSurface: fileport_makeport failed (fd=%d)", data->shm_fd); + return MACH_PORT_NULL; + } + data->shm_port = port; + return copySurfaceSendRight(data->shm_port); + } + + /* darling-lol M58cz: a surface with NO shared backing must STILL hand + * out a REAL mach send right. The old M57 code returned a fake token + * ((mach_port_t) sid); Chromium's IOSurfaceMachPortTraits then failed + * mach_port_mod_refs() with KERN_INVALID_NAME on every surface handoff, + * and the token could not be carried in a mach message at all. Back the + * right with /dev/null: the name is valid for mach_port_mod_refs() and + * mojo's port transport; a cross-process lookup of a private surface + * still resolves to a placeholder, as before. */ + { + int nullfd = open("/dev/null", 0 /* O_RDONLY */); + mach_port_t port = MACH_PORT_NULL; + if (nullfd >= 0 && + fileport_makeport(nullfd, &port) == KERN_SUCCESS && + port != MACH_PORT_NULL) + { + /* the fileport kobject owns nullfd now and closes it on death */ + data->shm_port = port; + return copySurfaceSendRight(data->shm_port); + } + if (nullfd >= 0) + close(nullfd); + } + return MACH_PORT_NULL; +} + +/* darling-lol M58b: IOSurfaceLookupFromMachPort() (in api.m) calls this when + * the port is a fileport holding a shared IOSurface backing: map the fd and + * reconstruct the object from the wire header at offset 0. */ +/* Keep at most eight live import mappings. File identity plus the complete + * validated wire layout prevents descriptor/Mach-name reuse from aliasing a + * different surface. The cache owns no Mach rights; every lookup still + * validates its caller's right through fileport_makefd before reaching here. */ +struct ImportedSurfaceCacheEntry { + uint64_t device, inode, size, age; + struct _IOSurfaceWireHeader wire; + IOSurface *surface; +}; +static struct ImportedSurfaceCacheEntry importedSurfaces[8]; +static uint64_t importedSurfaceAge; +extern void _IOSurfaceRegistryLock(void), _IOSurfaceRegistryUnlock(void); +static bool importedSurfaceMatches(struct ImportedSurfaceCacheEntry *entry, + const struct stat *st, const struct _IOSurfaceWireHeader *wire) { + return entry->surface && entry->device==(uint64_t)st->st_dev && + entry->inode==(uint64_t)st->st_ino && entry->size==(uint64_t)st->st_size && + !memcmp(&entry->wire,wire,sizeof(*wire)); +} +static IOSurface *cachedImportedSurface(const struct stat *st, + const struct _IOSurfaceWireHeader *wire, IOSurface *candidate) { + _IOSurfaceRegistryLock(); + for (unsigned i=0;i<8;i++) if(importedSurfaceMatches(&importedSurfaces[i],st,wire)) { + IOSurface *result=[importedSurfaces[i].surface retain]; + importedSurfaces[i].age=++importedSurfaceAge; + _IOSurfaceRegistryUnlock();[candidate release];return result; + } + if(!candidate){_IOSurfaceRegistryUnlock();return nil;} + unsigned index=0; + for(unsigned i=1;i<8;i++) if(importedSurfaces[i].agest_dev,.inode=(uint64_t)st->st_ino, + .size=(uint64_t)st->st_size,.age=++importedSurfaceAge, + .wire=*wire,.surface=[candidate retain]}; + _IOSurfaceRegistryUnlock();[old release];return candidate; +} + +IOSurfaceRef _IOSurfaceInitFromSharedMemfd(int fd) +{ + struct stat st; + if (fd < 0 || fstat(fd, &st) != 0 || st.st_size < (off_t) sizeof(struct _IOSurfaceWireHeader)) + { + if (fd >= 0) + close(fd); + return NULL; + } + + uint64_t shmSize = (uint64_t) st.st_size; + + struct _IOSurfaceWireHeader wire; + if (pread(fd, &wire, sizeof(wire), 0) != sizeof(wire) || + wire.magic != IOSURFACE_WIRE_MAGIC || + wire.version != IOSURFACE_WIRE_VERSION || + !wire.planeCount || wire.planeCount > 4 || + wire.memoryOffset != IOSURFACE_WIRE_DATA_OFFSET || + st.st_size < (off_t)(wire.memoryOffset + (uint64_t) wire.bytesPerRow * (uint64_t) wire.height)) + { + close(fd); + return NULL; + } + + for (unsigned i=0; i16 || + (uint64_t)p.width*p.bytesPerElement>p.bytesPerRow || + p.memoryOffsetshmSize || bytes>shmSize-p.memoryOffset) { + close(fd); return NULL; + } + } + unsigned planeCount=wire.planeCount; + bool cacheEnabled=getenv("RCLOL_IOSURFACE_IMPORT_CACHE")!=NULL; + if(cacheEnabled) { + IOSurface *cached=cachedImportedSurface(&st,&wire,nil); + if(cached){close(fd);return (IOSurfaceRef)cached;} + } + + void* address = mmap(NULL, shmSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (address == NULL || address == MAP_FAILED) + { + close(fd); + return NULL; + } + + size_t structSize = sizeof(struct _IOSurfaceObjectRetval) + (planeCount-1)*sizeof(struct _IOSurfacePlane); + struct _IOSurfaceObjectRetval* surface = calloc(1, structSize); + if (surface == NULL) + { + munmap(address, shmSize); + close(fd); + return NULL; + } + + surface->surfaceID = wire.surfaceID; + surface->pixelFormat = wire.pixelFormat; + surface->address = (uint64_t) address; + surface->planeCount=planeCount; + memcpy(surface->planes,wire.planes,planeCount*sizeof(struct _IOSurfacePlane)); + + IOSurface* obj = [IOSurface alloc]; + ImplData* idata = (ImplData*) malloc(sizeof(ImplData)); + idata->surface = surface; + idata->localUseCount = 0; + idata->shm_fd = fd; + idata->shm_size = shmSize; + idata->shm_port = MACH_PORT_NULL; + idata->attachments = [[NSMutableDictionary alloc] init]; + obj->_impl = idata; + + return (IOSurfaceRef)(cacheEnabled?cachedImportedSurface(&st,&wire,obj):obj); +} - (kern_return_t)lockWithOptions:(IOSurfaceLockOptions)options seed:(nullable uint32_t *)seed { ImplData* data = (ImplData*) _impl; @@ -269,6 +804,12 @@ typedef struct .surfaceID = data->surface->surfaceID, .options = options, }; + /* darling-lol: user-space fallback surfaces have no IOSurfaceRoot + * connection, so calling the (null) service returns an error and the + * frame path treats the surface as unusable. There is no real lock to + * take for a private allocation, so succeed. */ + if (g_surfaceService == 0) + return kIOSurfaceSuccess; return IOConnectCallMethod(g_surfaceService, kIOSurfaceMethodLock, NULL, 0, &args, sizeof(args), NULL, 0, NULL, 0); } @@ -279,6 +820,8 @@ typedef struct .surfaceID = data->surface->surfaceID, .options = options, }; + if (g_surfaceService == 0) + return kIOSurfaceSuccess; return IOConnectCallMethod(g_surfaceService, kIOSurfaceMethodUnlock, NULL, 0, &args, sizeof(args), NULL, 0, NULL, 0); } @@ -291,3 +834,8 @@ typedef struct } @end + +size_t _IOSurfaceObjCGetAllocSize(IOSurfaceRef buffer) { + ImplData *data=(ImplData*)((IOSurface*)buffer)->_impl; + return data->shm_fd>=0 ? data->shm_size-IOSURFACE_WIRE_DATA_OFFSET : data->shm_size; +} diff --git a/src/frameworks/IOSurface/src/api.m b/src/frameworks/IOSurface/src/api.m index dea76c18a..e873bcda5 100644 --- a/src/frameworks/IOSurface/src/api.m +++ b/src/frameworks/IOSurface/src/api.m @@ -19,6 +19,156 @@ #import #import +#import +#import +#import +#include +#include +#include +#include +#include + +/* darling-lol M58b: IOSurfaceCreateMachPort / IOSurfaceLookupFromMachPort are + * implemented on Darling's fileport machinery plus host shared-memory files; + * the send/receive helpers live in IOSurfaceObjC.m (they need the internal + * ImplData). The M57 fake-token registry is gone: fake names made Chromium's + * mach_port_mod_refs (ui/gfx/mac/io_surface.cc IOSurfaceMachPortTraits) fail + * with invalid_name on every surface handoff. */ +extern mach_port_t _IOSurfaceObjCShmFileportForSurface(IOSurfaceRef buffer); +extern IOSurfaceRef _IOSurfaceInitFromSharedMemfd(int fd); +extern int fileport_makefd(mach_port_t port); + +/* Keep the M57 token registry as the in-process fast path; the fileport is + * used when the surface has a shared backing. */ +static NSMutableDictionary* gMachPortRegistry = nil; +static NSRecursiveLock* gMachPortRegistryLock = nil; +static pthread_once_t registryOnce = PTHREAD_ONCE_INIT; + +/* darling-lol M58c: materialize the rendered GL texture into the IOSurface + * backing before the surface leaves this process. + * + * On macOS, CGLTexImageIOSurface2D makes the IOSurface the texture's storage, + * so GL rendering lands in IOSurface memory implicitly. Darling's CGL is + * backed by Mesa/EGL, where CGLTexImageIOSurface2D can only COPY the IOSurface + * into a private GL texture once at bind time. Chromium's compositor then + * renders every frame into that texture, and the IOSurface backing (the host + * shared-memory file shared with the next process) never receives the pixels — + * every published frame was metadata-correct but all-zero. + * + * The fix: at every cross-process handoff point (IOSurfaceCreateMachPort is + * called by the sender — GPU process for the display surface, renderer process + * for its GMB content surfaces), copy the bound GL texture back into the + * IOSurface memory via OpenGL.framework's CGLReadbackIOSurface. The binding + * table it consults is per-process, so this is a harmless no-op in processes + * that never GL-bound the surface (browser, software path). + * + * OpenGL.framework is resolved with a private dlopen: IOSurface cannot link + * OpenGL (OpenGL links IOSurface — a cycle), and per the M57bo lesson adding + * CGL symbols to IOSurface's dependency symbol scope would leak them to every + * dependent. In GL processes the framework is already loaded (ANGLE depends on + * it), so dlopen merely bumps the refcount. */ +typedef unsigned int _DarlingCGLError; +typedef _DarlingCGLError (*_DarlingReadbackFn)(IOSurfaceRef); + +static _DarlingReadbackFn _DarlingResolveReadback(void) +{ + static _DarlingReadbackFn fn; + static int resolved; + static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; + + pthread_mutex_lock(&lock); + if (!resolved) + { + resolved = 1; + void* h = dlopen("/System/Library/Frameworks/OpenGL.framework/OpenGL", + RTLD_NOW | RTLD_LOCAL); + if (h) + fn = (_DarlingReadbackFn) dlsym(h, "CGLReadbackIOSurface"); + } + pthread_mutex_unlock(&lock); + return fn; +} + +static void _DarlingWriteBackGLTexture(IOSurfaceRef buffer) +{ + _DarlingReadbackFn fn = _DarlingResolveReadback(); + if (!fn) + return; + _DarlingCGLError err = fn(buffer); + if (err != 0 && getenv("M58_WB_TRACE") != NULL) + fprintf(stderr, "M58WB pid=%d sid=%u readback=0x%x\n", + (int) getpid(), (unsigned) IOSurfaceGetID(buffer), (unsigned) err); +} + +static void initializeSurfaceRegistry(void) +{ + gMachPortRegistry = [[NSMutableDictionary alloc] init]; + gMachPortRegistryLock = [[NSRecursiveLock alloc] init]; +} +static void ensureMachPortRegistry(void) { pthread_once(®istryOnce, initializeSurfaceRegistry); } +void _IOSurfaceRegistryLock(void) { ensureMachPortRegistry(); [gMachPortRegistryLock lock]; } +void _IOSurfaceRegistryUnlock(void) { [gMachPortRegistryLock unlock]; } +void _IOSurfaceRegistryForget(IOSurfaceRef buffer) +{ + typedef void (*ForgetFn)(IOSurfaceRef); + ForgetFn forget = (ForgetFn)dlsym(RTLD_DEFAULT, "CGLForgetIOSurface"); + if (forget) forget(buffer); + NSNumber *key = [NSNumber numberWithUnsignedInt: IOSurfaceGetID(buffer)]; + NSValue *value = [gMachPortRegistry objectForKey:key]; + if ([value pointerValue] == buffer) [gMachPortRegistry removeObjectForKey:key]; +} +IOSurfaceRef _IOSurfaceLookupByID(IOSurfaceID sid) +{ + _IOSurfaceRegistryLock(); + NSValue *value = [gMachPortRegistry objectForKey:[NSNumber numberWithUnsignedInt:sid]]; + IOSurface *surface = (IOSurface*)[value pointerValue]; + [surface retain]; + _IOSurfaceRegistryUnlock(); + return (IOSurfaceRef)surface; +} + +mach_port_t IOSurfaceCreateMachPort(IOSurfaceRef buffer) +{ + if (buffer == NULL) + return MACH_PORT_NULL; + + /* darling-lol M58c: Chromium may have rendered this surface through a + * GL texture (CGLTexImageIOSurface2D's one-shot upload). Materialize + * the rendered texture into the IOSurface backing before the surface + * is handed to another process. No-op when this process never GL-bound + * the surface. */ + _DarlingWriteBackGLTexture(buffer); + + /* darling-lol M58b/M58cz: hand out a REAL mach send right. With a shared + * backing this is a fileport over the backing fd; with no shared backing + * _IOSurfaceObjCShmFileportForSurface() now also returns a real right + * (over /dev/null), so this never returns a fake token again. */ + mach_port_t port = _IOSurfaceObjCShmFileportForSurface(buffer); + if (port == MACH_PORT_NULL) + return MACH_PORT_NULL; + + /* ID lookup is distinct from Mach names. Final release is serialized + * with ID lookup, and deallocation removes this non-owning entry. */ + ensureMachPortRegistry(); + + IOSurfaceID sid = (IOSurfaceID) [(IOSurface*) buffer hash]; + if (sid == 0) + sid = 1; + + [gMachPortRegistryLock lock]; + [gMachPortRegistry setObject: [NSValue valueWithPointer: buffer] + forKey: [NSNumber numberWithUnsignedInt: sid]]; + [gMachPortRegistryLock unlock]; + + return port; +} + +IOSurfaceID IOSurfaceGetID(IOSurfaceRef buffer) +{ + if (buffer == NULL) + return 0; + return (IOSurfaceID) [(IOSurface*) buffer hash]; +} CFTypeID IOSurfaceGetTypeID(void) { @@ -32,7 +182,22 @@ IOSurfaceRef _Nullable IOSurfaceCreate(CFDictionaryRef properties) IOSurfaceRef _Nullable IOSurfaceLookupFromMachPort(mach_port_t port) { - return (IOSurfaceRef) [[IOSurface alloc] initWithMachPort: port]; + if (port == MACH_PORT_NULL) + return NULL; + + /* Mach names are reusable and must never index a weak object cache. + * Reconstruct from the send right's shared backing, including in-process. */ + /* darling-lol M58b: the port is a fileport holding the surface's + * shared backing; fileport_makefd() gives this process its own dup of + * that fd, which the helper takes ownership of (mmap + object + * construction from the wire header). */ + int fd = fileport_makefd(port); + if (fd >= 0) + { + return _IOSurfaceInitFromSharedMemfd(fd); + } + + return NULL; } xpc_object_t IOSurfaceCreateXPCObject(IOSurfaceRef surface) @@ -57,9 +222,35 @@ kern_return_t IOSurfaceLock(IOSurfaceRef buffer, IOSurfaceLockOptions options, u return [(IOSurface*)buffer lockWithOptions: options seed: seed]; } +static void _DarlingPublishSoftwarePixels(IOSurfaceRef surface) { + typedef void (*PublishFn)(IOSurfaceRef); + static PublishFn publish; + static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; + pthread_mutex_lock(&lock); + if (!publish) { + /* Avoid introducing a framework dependency cycle or loading CA into + * processes that do not use it. Retry if it was not yet loaded. */ + publish = (PublishFn)dlsym(RTLD_DEFAULT, "DarlingCAPublishIOSurfacePixels"); + } + PublishFn callback = publish; + pthread_mutex_unlock(&lock); + if (callback) callback(surface); +} + +/* GPU readback completes a lock without claiming software compositor ownership. */ +kern_return_t _IOSurfaceUnlockGPUReadback(IOSurfaceRef buffer) +{ + return [(IOSurface*)buffer unlockWithOptions:0 seed:NULL]; +} + kern_return_t IOSurfaceUnlock(IOSurfaceRef buffer, IOSurfaceLockOptions options, uint32_t * _Nullable seed) { - return [(IOSurface*)buffer unlockWithOptions: options seed: seed]; + kern_return_t result = [(IOSurface*)buffer unlockWithOptions: options seed: seed]; + /* A software compositor reuses its IOSurface without a CALayer setter. + * Publish completed writes, excluding the read-only snapshot locks. */ + if (result == KERN_SUCCESS && !(options & kIOSurfaceLockReadOnly)) + _DarlingPublishSoftwarePixels(buffer); + return result; } size_t IOSurfaceGetWidth(IOSurfaceRef buffer) diff --git a/src/frameworks/IOSurface/src/stubs.m b/src/frameworks/IOSurface/src/stubs.m index 7e5976f2f..bd2566180 100644 --- a/src/frameworks/IOSurface/src/stubs.m +++ b/src/frameworks/IOSurface/src/stubs.m @@ -23,6 +23,12 @@ #include +/* darling-lol M57: IOSurfaceLookup has to resolve IDs produced by our + * user-space IOSurface fallback (IOSurfaceCreateMachPort registers tokens in + * api.m). Returning 0 here broke IOSurface lookup-by-ID and the + * initWithSurfaceID: recovery path used during frame handoff. */ +IOSurfaceRef _Nullable IOSurfaceLookupFromMachPort(mach_port_t port); + static int verbose = 0; __attribute__((constructor)) @@ -33,19 +39,23 @@ static void initme(void) { IOSurfaceRef _Nullable IOSurfaceLookup(IOSurfaceID csid){ if (verbose) printf("STUB: %s called\n", __FUNCTION__); - return 0; -} - - -IOSurfaceID IOSurfaceGetID(IOSurfaceRef buffer){ - if (verbose) printf("STUB: %s called\n", __FUNCTION__); - return 0; + if (csid == 0) + return 0; + extern IOSurfaceRef _IOSurfaceLookupByID(IOSurfaceID); + return _IOSurfaceLookupByID(csid); } size_t IOSurfaceGetAllocSize(IOSurfaceRef buffer){ if (verbose) printf("STUB: %s called\n", __FUNCTION__); - return 0; + if (buffer == NULL) + return 0; + /* darling-lol: Chromium checks the allocation size when wrapping a + * surface in a GpuMemoryBuffer; returning 0 made every buffer look + * invalid. The user-space fallback allocates exactly + * bytesPerRow * height for plane 0. */ + extern size_t _IOSurfaceObjCGetAllocSize(IOSurfaceRef); + return _IOSurfaceObjCGetAllocSize(buffer); } @@ -93,12 +103,6 @@ IOSurfaceSubsampling IOSurfaceGetSubsampling(IOSurfaceRef buffer){ -mach_port_t IOSurfaceCreateMachPort(IOSurfaceRef buffer){ - if (verbose) printf("STUB: %s called\n", __FUNCTION__); - return 0; -} - - size_t IOSurfaceGetPropertyMaximum(CFStringRef property){ if (verbose) printf("STUB: %s called\n", __FUNCTION__); return 0; @@ -112,8 +116,17 @@ size_t IOSurfaceGetPropertyAlignment(CFStringRef property){ size_t IOSurfaceAlignProperty(CFStringRef property, size_t value){ - if (verbose) printf("STUB: %s called\n", __FUNCTION__); - return 0; + /* Our shared backing requires valid non-overlapping plane layouts. + * Chromium aligns every plane stride, allocation and offset through this + * API; returning zero makes all chroma planes overlap the luma plane. */ + size_t alignment = 1; + if(property && CFEqual(property,kIOSurfaceAllocSize)) alignment=4096; + else if(property && (CFEqual(property,kIOSurfaceBytesPerRow) || + CFEqual(property,kIOSurfacePlaneBytesPerRow) || + CFEqual(property,kIOSurfacePlaneSize) || + CFEqual(property,kIOSurfacePlaneOffset))) alignment=64; + if(value>SIZE_MAX-(alignment-1))return 0; + return (value+alignment-1)&~(alignment-1); } @@ -123,4 +136,3 @@ Boolean IOSurfaceAllowsPixelSizeCasting(IOSurfaceRef buffer){ return 0; } - diff --git a/src/frameworks/ImageCaptureCore/include/ImageCaptureCore/ICCameraItem.h b/src/frameworks/ImageCaptureCore/include/ImageCaptureCore/ICCameraItem.h index 1b9216d1e..9e9f35386 100644 --- a/src/frameworks/ImageCaptureCore/include/ImageCaptureCore/ICCameraItem.h +++ b/src/frameworks/ImageCaptureCore/include/ImageCaptureCore/ICCameraItem.h @@ -22,6 +22,10 @@ typedef NSString* ICDownloadOption; extern const ICDownloadOption ICDownloadsDirectoryURL; +extern const ICDownloadOption ICOverwrite; +extern const ICDownloadOption ICSaveAsFilename; +extern const ICDownloadOption ICSavedFilename; +extern const ICDownloadOption ICTransportTypeMassStorage; @interface ICCameraItem : NSObject diff --git a/src/frameworks/ImageCaptureCore/src/ICCameraItem.m b/src/frameworks/ImageCaptureCore/src/ICCameraItem.m index 350ae5f9a..0ca4ea610 100644 --- a/src/frameworks/ImageCaptureCore/src/ICCameraItem.m +++ b/src/frameworks/ImageCaptureCore/src/ICCameraItem.m @@ -20,6 +20,10 @@ #import ICDownloadOption const ICDownloadsDirectoryURL = @"ICDownloadsDirectoryURL"; +ICDownloadOption const ICOverwrite = @"ICOverwrite"; +ICDownloadOption const ICSaveAsFilename = @"ICSaveAsFilename"; +ICDownloadOption const ICSavedFilename = @"ICSavedFilename"; +ICDownloadOption const ICTransportTypeMassStorage = @"MassStorage"; @implementation ICCameraItem diff --git a/src/frameworks/MediaAccessibility/CMakeLists.txt b/src/frameworks/MediaAccessibility/CMakeLists.txt index 216be5050..2edeaac81 100644 --- a/src/frameworks/MediaAccessibility/CMakeLists.txt +++ b/src/frameworks/MediaAccessibility/CMakeLists.txt @@ -22,4 +22,5 @@ add_framework(MediaAccessibility DEPENDENCIES system + Foundation ) diff --git a/src/frameworks/MediaAccessibility/include/MediaAccessibility/MediaAccessibility.h b/src/frameworks/MediaAccessibility/include/MediaAccessibility/MediaAccessibility.h index 6be755c16..7916d13b1 100644 --- a/src/frameworks/MediaAccessibility/include/MediaAccessibility/MediaAccessibility.h +++ b/src/frameworks/MediaAccessibility/include/MediaAccessibility/MediaAccessibility.h @@ -264,6 +264,10 @@ void* MAXCaptionAppearancePrefSetWindowColor(void); void* MAXCaptionAppearancePrefSetWindowOpacity(void); void* MAXCaptionAppearancePrefSetWindowRoundedCornerRadius(void); void* MAXCaptionAppearanceSetPreferredLanguage(void); +#include + void* MAXCaptionAppearanceShowCaptionsWhenAvailable(void); +extern const CFStringRef kMACaptionAppearanceSettingsChangedNotification; + #endif diff --git a/src/frameworks/MediaAccessibility/src/MediaAccessibility.c b/src/frameworks/MediaAccessibility/src/MediaAccessibility.c index d2b138594..62cc3c784 100644 --- a/src/frameworks/MediaAccessibility/src/MediaAccessibility.c +++ b/src/frameworks/MediaAccessibility/src/MediaAccessibility.c @@ -19,11 +19,14 @@ #include +#include #include #include static int verbose = 0; +const CFStringRef kMACaptionAppearanceSettingsChangedNotification = CFSTR("kMACaptionAppearanceSettingsChangedNotification"); + __attribute__((constructor)) static void initme(void) { verbose = getenv("STUB_VERBOSE") != NULL; diff --git a/src/frameworks/OpenDirectory/CMakeLists.txt b/src/frameworks/OpenDirectory/CMakeLists.txt index b20ad7782..6f8d36d02 100644 --- a/src/frameworks/OpenDirectory/CMakeLists.txt +++ b/src/frameworks/OpenDirectory/CMakeLists.txt @@ -21,6 +21,7 @@ add_framework(OpenDirectory SOURCES src/OpenDirectory.m + src/ODLocalDirectory.m src/NSODSession.m src/ODSession.m src/NSODNode.m diff --git a/src/frameworks/OpenDirectory/src/ODNode.m b/src/frameworks/OpenDirectory/src/ODNode.m index 24243f915..5731f9783 100644 --- a/src/frameworks/OpenDirectory/src/ODNode.m +++ b/src/frameworks/OpenDirectory/src/ODNode.m @@ -19,7 +19,89 @@ #import +#import "ODLocalDirectory.h" + @implementation ODNode +{ + NSString *_nodeName; +} + ++ (instancetype)nodeWithSession:(ODSession *)session type:(ODNodeType)type error:(NSError **)error +{ + return [[[self alloc] initWithSession: session type: type error: error] autorelease]; +} + ++ (instancetype)nodeWithSession:(ODSession *)session name:(NSString *)name error:(NSError **)error +{ + return [[[self alloc] initWithSession: session name: name error: error] autorelease]; +} + +- (instancetype)initWithSession:(ODSession *)session type:(ODNodeType)type error:(NSError **)error +{ + if (error != NULL) + *error = nil; + if ((self = [super init]) != nil) + _nodeName = [ODLocalNodeNameForType(type) copy]; + return self; +} + +- (instancetype)initWithSession:(ODSession *)session name:(NSString *)name error:(NSError **)error +{ + NSString *known = ODLocalNodeNameForName(name); + + if (error != NULL) + *error = known != nil ? nil : ODLocalError(kODErrorNodeUnknownName); + if (known == nil) { + [self release]; + return nil; + } + if ((self = [super init]) != nil) + _nodeName = [known copy]; + return self; +} + +- (void)dealloc +{ + [_nodeName release]; + [super dealloc]; +} + +- (NSString *)nodeName +{ + return _nodeName; +} + +- (NSArray *)subnodeNamesAndReturnError:(NSError **)error +{ + if (error != NULL) + *error = nil; + return [_nodeName isEqualToString: ODLocalNodeName] ? @[] : @[ODLocalNodeName]; +} + +- (NSArray *)unreachableSubnodeNamesAndReturnError:(NSError **)error +{ + if (error != NULL) + *error = nil; + return nil; +} + +- (NSArray *)supportedRecordTypesAndReturnError:(NSError **)error +{ + if (error != NULL) + *error = nil; + return @[kODRecordTypeUsers]; +} + +- (ODRecord *)recordWithRecordType:(ODRecordType)recordType name:(NSString *)name attributes:(id)attributes error:(NSError **)error +{ + if (error != NULL) + *error = nil; + if (!ODLocalRecordTypesIncludeUsers(recordType)) + return nil; + + NSDictionary *user = ODLocalUserAttributes(name); + return user != nil ? [ODLocalRecordCreate(user) autorelease] : nil; +} - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { diff --git a/src/frameworks/OpenDirectory/src/ODQuery.m b/src/frameworks/OpenDirectory/src/ODQuery.m index 5b0361e37..91e5b3ebb 100644 --- a/src/frameworks/OpenDirectory/src/ODQuery.m +++ b/src/frameworks/OpenDirectory/src/ODQuery.m @@ -19,7 +19,143 @@ #import +#import +#import "ODLocalDirectory.h" + @implementation ODQuery +{ + BOOL _users; + NSString *_attribute; + ODMatchType _matchType; + NSArray *_queryValues; + NSInteger _maximumResults; +} + ++ (ODQuery *)queryWithNode:(ODNode *)node forRecordTypes:(id)recordTypeOrList attribute:(ODAttributeType)attribute + matchType:(ODMatchType)matchType queryValues:(id)queryValueOrList + returnAttributes:(id)returnAttributeOrList maximumResults:(NSInteger)maximumResults error:(NSError **)error +{ + return [[[self alloc] initWithNode: node forRecordTypes: recordTypeOrList attribute: attribute + matchType: matchType queryValues: queryValueOrList + returnAttributes: returnAttributeOrList maximumResults: maximumResults error: error] autorelease]; +} + +- (instancetype)initWithNode:(ODNode *)node forRecordTypes:(id)recordTypeOrList attribute:(ODAttributeType)attribute + matchType:(ODMatchType)matchType queryValues:(id)queryValueOrList + returnAttributes:(id)returnAttributeOrList maximumResults:(NSInteger)maximumResults error:(NSError **)error +{ + if (error != NULL) + *error = nil; + if (node == nil) { + if (error != NULL) + *error = ODLocalError(kODErrorNodeUnknownName); + [self release]; + return nil; + } + if ((self = [super init]) != nil) { + NSMutableArray *values = [NSMutableArray array]; + NSArray *given = [queryValueOrList isKindOfClass: [NSArray class]] ? queryValueOrList + : queryValueOrList != nil ? @[queryValueOrList] : @[]; + + for (id value in given) { + if ([value isKindOfClass: [NSData class]]) + value = [[[NSString alloc] initWithData: value encoding: NSUTF8StringEncoding] autorelease]; + if ([value isKindOfClass: [NSString class]]) + [values addObject: value]; + } + + _users = ODLocalRecordTypesIncludeUsers(recordTypeOrList); + _attribute = [attribute copy]; + _matchType = matchType; + _queryValues = [values copy]; + _maximumResults = maximumResults; + } + return self; +} + +- (void)dealloc +{ + [_attribute release]; + [_queryValues release]; + [super dealloc]; +} + +- (id)copyWithZone:(NSZone *)zone +{ + ODQuery *copy = [[ODQuery allocWithZone: zone] init]; + + if (copy != nil) { + copy->_users = _users; + copy->_attribute = [_attribute copy]; + copy->_matchType = _matchType; + copy->_queryValues = [_queryValues copy]; + copy->_maximumResults = _maximumResults; + } + return copy; +} + +- (BOOL)_value:(NSString *)value matches:(NSString *)wanted +{ + NSStringCompareOptions options = (_matchType & 0x0100) ? NSCaseInsensitiveSearch : 0; + + switch (_matchType & ~0x0100) { + case kODMatchEqualTo: + return [value compare: wanted options: options] == NSOrderedSame; + case kODMatchBeginsWith: + return [value rangeOfString: wanted options: options | NSAnchoredSearch].location != NSNotFound; + case kODMatchEndsWith: + return [value rangeOfString: wanted options: options | NSAnchoredSearch | NSBackwardsSearch].location != NSNotFound; + case kODMatchContains: + return [value rangeOfString: wanted options: options].location != NSNotFound; + default: + return NO; + } +} + +- (BOOL)_matches:(NSDictionary *)user +{ + if (_matchType == kODMatchAny) + return YES; + + for (NSString *value in user[_attribute]) { + for (NSString *wanted in _queryValues) { + if ([self _value: value matches: wanted]) + return YES; + } + } + return NO; +} + +- (NSArray *)resultsAllowingPartial:(BOOL)allowPartial error:(NSError **)error +{ + NSMutableArray *results = [NSMutableArray array]; + NSArray *candidates = @[]; + + if (error != NULL) + *error = nil; + if (!_users || (_attribute == nil && _matchType != kODMatchAny)) + return results; + + if (_matchType == kODMatchEqualTo && [_attribute isEqualToString: kODAttributeTypeRecordName]) { + NSMutableArray *named = [NSMutableArray array]; + for (NSString *name in _queryValues) { + NSDictionary *user = ODLocalUserAttributes(name); + if (user != nil) + [named addObject: user]; + } + candidates = named; + } else { + candidates = ODLocalAllUserAttributes(); + } + + for (NSDictionary *user in candidates) { + if (_maximumResults > 0 && [results count] >= (NSUInteger)_maximumResults) + break; + if ([self _matches: user]) + [results addObject: [ODLocalRecordCreate(user) autorelease]]; + } + return results; +} - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { diff --git a/src/frameworks/OpenDirectory/src/ODRecord.m b/src/frameworks/OpenDirectory/src/ODRecord.m index 5e207c4bb..5c7bd9a14 100644 --- a/src/frameworks/OpenDirectory/src/ODRecord.m +++ b/src/frameworks/OpenDirectory/src/ODRecord.m @@ -19,7 +19,63 @@ #import +#import "ODLocalDirectory.h" + @implementation ODRecord +{ + NSDictionary *_attributes; +} + +ODRecord *ODLocalRecordCreate(NSDictionary *attributes) +{ + ODRecord *record = [[ODRecord alloc] init]; + + if (record != nil) + record->_attributes = [attributes copy]; + return record; +} + +- (void)dealloc +{ + [_attributes release]; + [super dealloc]; +} + +- (NSString *)recordType +{ + return [_attributes[@"dsAttrTypeStandard:RecordType"] firstObject]; +} + +- (NSString *)recordName +{ + return [_attributes[@"dsAttrTypeStandard:RecordName"] firstObject]; +} + +- (NSArray *)valuesForAttribute:(ODAttributeType)attribute error:(NSError **)error +{ + if (error != NULL) + *error = nil; + if (attribute == nil) + return nil; + return _attributes[attribute] ?: @[]; +} + +- (NSDictionary *)recordDetailsForAttributes:(NSArray *)attributes error:(NSError **)error +{ + if (error != NULL) + *error = nil; + if (attributes == nil || [attributes containsObject: @"dsAttributesAll"] + || [attributes containsObject: @"dsAttributesStandardAll"]) + return _attributes; + + NSMutableDictionary *details = [NSMutableDictionary dictionary]; + for (id attribute in attributes) { + NSArray *values = _attributes[attribute]; + if (values != nil) + details[attribute] = values; + } + return details; +} - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { diff --git a/src/frameworks/OpenDirectory/src/ODSession.m b/src/frameworks/OpenDirectory/src/ODSession.m index aff236828..c9e628d43 100644 --- a/src/frameworks/OpenDirectory/src/ODSession.m +++ b/src/frameworks/OpenDirectory/src/ODSession.m @@ -21,8 +21,48 @@ #import +#import "ODLocalDirectory.h" + @implementation ODSession ++ (ODSession *)defaultSession +{ + static ODSession *shared; + static dispatch_once_t once; + + dispatch_once(&once, ^{ + shared = [[ODSession alloc] init]; + }); + return shared; +} + ++ (instancetype)sessionWithOptions:(NSDictionary *)options error:(NSError **)error +{ + return [[[self alloc] initWithOptions: options error: error] autorelease]; +} + +- (instancetype)initWithOptions:(NSDictionary *)options error:(NSError **)error +{ + if (error != NULL) + *error = nil; + + // only the local directory exists; there is no proxy to connect through + if ([options count] != 0) { + if (error != NULL) + *error = ODLocalError(kODErrorSessionProxyCommunicationError); + [self release]; + return nil; + } + return [self init]; +} + +- (NSArray *)nodeNamesAndReturnError:(NSError **)error +{ + if (error != NULL) + *error = nil; + return @[ODLocalNodeName]; +} + - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { return [NSMethodSignature signatureWithObjCTypes: "v@:"]; diff --git a/src/frameworks/OpenGL/CMakeLists.txt b/src/frameworks/OpenGL/CMakeLists.txt index deca16b87..d75d5e22d 100644 --- a/src/frameworks/OpenGL/CMakeLists.txt +++ b/src/frameworks/OpenGL/CMakeLists.txt @@ -31,9 +31,14 @@ add_framework(OpenGL system GL GLU - EGL + # darling-lol M57bo: EGL is deliberately NOT a link dependency. Linking + # it here puts the native Mesa EGL in the dependency symbol scope of + # OpenGL.framework's dependents (ANGLE's libGLESv2), so Chromium's EGL + # lookups resolve to the native EGL instead of ANGLE's. OpenGL.c loads + # it privately with dlopen(RTLD_LOCAL) instead. CoreFoundation CoreGraphics + IOSurface ) set_property(TARGET OpenGL APPEND_STRING PROPERTY LINK_FLAGS diff --git a/src/frameworks/OpenGL/OpenGL.c b/src/frameworks/OpenGL/OpenGL.c index ffd7fe054..a40a634d3 100644 --- a/src/frameworks/OpenGL/OpenGL.c +++ b/src/frameworks/OpenGL/OpenGL.c @@ -2,11 +2,22 @@ #include // for fprintf(stderr, "unimplemented") #include - +#include +#include +#include +#include +#include +#include +extern long __lolsys_syscall(long,long,long,long,long,long,int); + +#define GL_GLEXT_PROTOTYPES 1 #include +#include #include #include +#include #include +#include // Try to get the right (generic) type definitions. // In particular, we really want EGLNativeDisplayType to be void *, @@ -22,8 +33,73 @@ #define APPLE #define __APPLE__ +/* darling-lol M57bo: load the native EGL privately. + * + * OpenGL.framework is a load-time dependency of ANGLE's libGLESv2 (its GL + * backend). If the native EGL were linked here, its egl* symbols would land in + * libGLESv2's dependency symbol scope and satisfy Chromium's EGL lookups, so + * Chromium would pair Darling's Mesa EGL with ANGLE's GL - eglMakeCurrent would + * never make an ANGLE gl::Context current (GL_MAX_VERTEX_ATTRIBS = 0, no + * frames). Resolving EGL through a private RTLD_LOCAL dlopen keeps it out of + * dependents' scope, so Chromium falls back to ANGLE's own EGL. */ +#include + +static EGLDisplay (*p_eglGetDisplay)(EGLNativeDisplayType); +static EGLDisplay (*p_eglGetPlatformDisplay)(EGLenum, void *, const EGLAttrib *); +static EGLBoolean (*p_eglInitialize)(EGLDisplay, EGLint *, EGLint *); +static EGLBoolean (*p_eglQuerySurface)(EGLDisplay,EGLSurface,EGLint,EGLint*); +static EGLBoolean (*p_eglGetConfigAttrib)(EGLDisplay,EGLConfig,EGLint,EGLint*); +static EGLBoolean (*p_eglChooseConfig)(EGLDisplay, const EGLint *, EGLConfig *, EGLint, EGLint *); +static EGLBoolean (*p_eglBindAPI)(EGLenum); +static EGLSurface (*p_eglCreateWindowSurface)(EGLDisplay, EGLConfig, EGLNativeWindowType, const EGLint *); +static EGLBoolean (*p_eglDestroySurface)(EGLDisplay, EGLSurface); +static EGLBoolean (*p_eglMakeCurrent)(EGLDisplay, EGLSurface, EGLSurface, EGLContext); +static EGLContext (*p_eglCreateContext)(EGLDisplay, EGLConfig, EGLContext, const EGLint *); +static EGLBoolean (*p_eglDestroyContext)(EGLDisplay, EGLContext); +static EGLBoolean (*p_eglSwapBuffers)(EGLDisplay, EGLSurface); +static EGLBoolean (*p_eglSwapInterval)(EGLDisplay, EGLint); + +static int native_egl_loaded; +static pthread_mutex_t native_egl_mutex = PTHREAD_MUTEX_INITIALIZER; + +static void ensure_native_egl(void) { + if (native_egl_loaded) + return; + pthread_mutex_lock(&native_egl_mutex); + if (!native_egl_loaded) { + void *h = dlopen("/usr/lib/native/libEGL.dylib", RTLD_NOW | RTLD_LOCAL); + if (h) { + p_eglGetDisplay = (EGLDisplay (*)(EGLNativeDisplayType)) dlsym(h, "eglGetDisplay"); + p_eglGetPlatformDisplay = (EGLDisplay (*)(EGLenum, void *, const EGLAttrib *)) dlsym(h, "eglGetPlatformDisplay"); + p_eglInitialize = (EGLBoolean (*)(EGLDisplay, EGLint *, EGLint *)) dlsym(h, "eglInitialize"); + p_eglQuerySurface=(EGLBoolean(*)(EGLDisplay,EGLSurface,EGLint,EGLint*))dlsym(h,"eglQuerySurface"); + p_eglGetConfigAttrib=(EGLBoolean(*)(EGLDisplay,EGLConfig,EGLint,EGLint*))dlsym(h,"eglGetConfigAttrib"); + p_eglChooseConfig = (EGLBoolean (*)(EGLDisplay, const EGLint *, EGLConfig *, EGLint, EGLint *)) dlsym(h, "eglChooseConfig"); + p_eglBindAPI = (EGLBoolean (*)(EGLenum)) dlsym(h, "eglBindAPI"); + p_eglCreateWindowSurface = (EGLSurface (*)(EGLDisplay, EGLConfig, EGLNativeWindowType, const EGLint *)) dlsym(h, "eglCreateWindowSurface"); + p_eglDestroySurface = (EGLBoolean (*)(EGLDisplay, EGLSurface)) dlsym(h, "eglDestroySurface"); + p_eglMakeCurrent = (EGLBoolean (*)(EGLDisplay, EGLSurface, EGLSurface, EGLContext)) dlsym(h, "eglMakeCurrent"); + p_eglCreateContext = (EGLContext (*)(EGLDisplay, EGLConfig, EGLContext, const EGLint *)) dlsym(h, "eglCreateContext"); + p_eglDestroyContext = (EGLBoolean (*)(EGLDisplay, EGLContext)) dlsym(h, "eglDestroyContext"); + p_eglSwapBuffers = (EGLBoolean (*)(EGLDisplay, EGLSurface)) dlsym(h, "eglSwapBuffers"); + p_eglSwapInterval = (EGLBoolean (*)(EGLDisplay, EGLint)) dlsym(h, "eglSwapInterval"); + native_egl_loaded = 1; + } + } + pthread_mutex_unlock(&native_egl_mutex); +} + static EGLDisplay display; +static EGLDisplay x11EGLDisplay(EGLNativeDisplayType native_display) { + /* CGS/AppKit supply Xlib Display pointers. Mesa's automatic platform + * selection can choose Wayland on a Wayland desktop and interpret the + * Xlib pointer as a wl_display, crashing during eglInitialize. */ + if (p_eglGetPlatformDisplay) + return p_eglGetPlatformDisplay(0x31D5 /* EGL_PLATFORM_X11 */, native_display, NULL); + return p_eglGetDisplay(native_display); +} + static EGLConfig config; static int num_config; @@ -48,10 +124,19 @@ struct _CGLContextObj { GLuint retain_count; pthread_mutex_t lock; EGLContext egl_context; + EGLDisplay egl_display; + EGLConfig egl_config; EGLSurface egl_surface; // EGL has no function for getting the current swap interval, // so we need to save the last set value. The default is 1. int swap_interval; + int swap_interval_requested; + EGLSurface native_present_surface; + EGLContext native_present_context; + DarlingNativePresentTarget *native_present_target; + uint32_t native_present_window; + uint64_t native_next_present_ns; + struct DarlingAsyncPresent *native_async; }; struct _CGLPixelFormatObj { @@ -59,6 +144,24 @@ struct _CGLPixelFormatObj { CGLPixelFormatAttribute *attributes; }; +typedef struct { + IOSurfaceID surfaceID; + CGLContextObj context; + GLenum target; + GLenum format; + GLenum type; + GLuint texture; + GLuint plane; + IOSurfaceRef surface; /* non-owning, used only to invalidate this binding */ +} CGLIOSurfaceBinding; + +static void stopAsyncPresent(CGLContextObj context); + +static CGLIOSurfaceBinding initialSurfaceBindings[256]; +static CGLIOSurfaceBinding *ioSurfaceBindings = initialSurfaceBindings; +static unsigned ioSurfaceBindingCount, ioSurfaceBindingCapacity = 256; +static pthread_mutex_t ioSurfaceBindingMutex = PTHREAD_MUTEX_INITIALIZER; + static inline int attribute_has_argument(CGLPixelFormatAttribute attr) { switch (attr) { case kCGLPFAAuxBuffers: @@ -69,6 +172,9 @@ static inline int attribute_has_argument(CGLPixelFormatAttribute attr) { case kCGLPFAAccumSize: case kCGLPFARendererID: case kCGLPFADisplayMask: + case kCGLPFAOpenGLProfile: + case kCGLPFASampleBuffers: + case kCGLPFASamples: return 1; default: return 0; @@ -78,6 +184,7 @@ static inline int attribute_has_argument(CGLPixelFormatAttribute attr) { __attribute__((constructor)) static void _CGLInitialize(void) { + ensure_native_egl(); g_displays = CFDictionaryCreateMutable(NULL, 0, NULL, NULL); } @@ -93,41 +200,71 @@ static int attributes_count(const CGLPixelFormatAttribute *attrs) { CGLError CGLRegisterNativeDisplay(void *native_display) { - display = eglGetDisplay(native_display); + ensure_native_egl(); + display = x11EGLDisplay(native_display); if (display == EGL_NO_DISPLAY) { return kCGLBadConnection; } - eglInitialize(display, NULL, NULL); - eglChooseConfig(display, attribute_list, &config, 1, &num_config); + p_eglInitialize(display, NULL, NULL); + p_eglChooseConfig(display, attribute_list, &config, 1, &num_config); - eglBindAPI(EGL_OPENGL_API); + p_eglBindAPI(EGL_OPENGL_API); return kCGLNoError; } +/* darling-lol M57: processes that create a CGL context without first calling + * CGLRegisterNativeDisplay (e.g. Chromium's GPU process) previously got + * display==0/config==0 and CGLCreateContext always failed. Lazily initialise a + * default EGL display so context creation can proceed. */ +static void ensureDefaultDisplay(void) { + ensure_native_egl(); + if (display != EGL_NO_DISPLAY && display != NULL && config != NULL) + return; + + void *nativeDisplay=_CGSNativeDisplay(CGSMainConnectionID()); + EGLDisplay disp = x11EGLDisplay(nativeDisplay?nativeDisplay:EGL_DEFAULT_DISPLAY); + if (disp == EGL_NO_DISPLAY) { + fprintf(stderr, "M58DG ensureDefaultDisplay: eglGetDisplay(DEFAULT)=NO_DISPLAY (p_eglGetDisplay=%p)\n", (void*) p_eglGetDisplay); + return; + } + + EGLConfig cfg = NULL; + EGLBoolean m58dg_ini = p_eglInitialize(disp, NULL, NULL); + EGLBoolean m58dg_ch = p_eglChooseConfig(disp, attribute_list, &cfg, 1, &num_config); + fprintf(stderr, "M58DG ensureDefaultDisplay: disp=%p eglInitialize=%d eglChooseConfig=%d cfg=%p num_config=%d\n", + (void*) disp, (int) m58dg_ini, (int) m58dg_ch, (void*) cfg, num_config); + if (m58dg_ini && m58dg_ch && cfg != NULL) { + display = disp; + config = cfg; + p_eglBindAPI(EGL_OPENGL_API); + } +} + static struct _CGLDisplay* getCGLDisplay(CGSConnectionID cid) { struct _CGLDisplay* rv; + ensure_native_egl(); pthread_mutex_lock(&g_displaysMutex); rv = (struct _CGLDisplay*) CFDictionaryGetValue(g_displays, (const void*)(unsigned long) cid); pthread_mutex_unlock(&g_displaysMutex); if (!rv) { - EGLDisplay disp = eglGetDisplay(_CGSNativeDisplay(cid)); + EGLDisplay disp = x11EGLDisplay(_CGSNativeDisplay(cid)); if (disp == EGL_NO_DISPLAY) return NULL; rv = (struct _CGLDisplay*) malloc(sizeof(*rv)); rv->display = disp; - eglInitialize(rv->display, NULL, NULL); - eglChooseConfig(rv->display, attribute_list, &rv->config, 1, &rv->num_config); + p_eglInitialize(rv->display, NULL, NULL); + p_eglChooseConfig(rv->display, attribute_list, &rv->config, 1, &rv->num_config); - eglBindAPI(EGL_OPENGL_API); + p_eglBindAPI(EGL_OPENGL_API); pthread_mutex_lock(&g_displaysMutex); CFDictionaryAddValue(g_displays, (const void*)(unsigned long) cid, rv); @@ -139,6 +276,8 @@ static struct _CGLDisplay* getCGLDisplay(CGSConnectionID cid) CGLError CGLSetSurface(CGLContextObj gl, CGSConnectionID cid, CGSWindowID wid, CGSSurfaceID sid) { + if (!gl) + return kCGLBadContext; struct _CGLDisplay* disp = getCGLDisplay(cid); if (!disp) return kCGLBadConnection; @@ -152,7 +291,7 @@ CGLError CGLSetSurface(CGLContextObj gl, CGSConnectionID cid, CGSWindowID wid, C if (!window) return kCGLBadWindow; - gl->egl_surface = eglCreateWindowSurface(disp->display, disp->config, window, NULL); + gl->egl_surface = p_eglCreateWindowSurface(gl->egl_display, gl->egl_config, window, NULL); if (gl->egl_surface == EGL_NO_SURFACE) return kCGLBadState; return kCGLNoError; @@ -161,7 +300,7 @@ CGLError CGLSetSurface(CGLContextObj gl, CGSConnectionID cid, CGSWindowID wid, C CGLWindowRef CGLGetWindow(void *native_window) { EGLNativeWindowType window = (EGLNativeWindowType) native_window; - EGLSurface surface = eglCreateWindowSurface(display, config, window, NULL); + EGLSurface surface = p_eglCreateWindowSurface(display, config, window, NULL); if (surface == EGL_NO_SURFACE) { return NULL; @@ -170,15 +309,53 @@ CGLWindowRef CGLGetWindow(void *native_window) { return (CGLWindowRef) surface; } +/* The default config has no alpha, and NVIDIA rejects it on 32-bit windows (EGL_BAD_CONFIG). + * Translucent windows need an alpha config whose native visual is the window's. */ +CGLWindowRef CGLGetWindowWithAlpha(void *native_window, unsigned long visual_id) { + static EGLint const alpha_attribute_list[] = { + EGL_RED_SIZE, 1, + EGL_GREEN_SIZE, 1, + EGL_BLUE_SIZE, 1, + EGL_ALPHA_SIZE, 1, + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, + EGL_NONE + }; + EGLint count = 0; + if (!display || !p_eglGetConfigAttrib || + !p_eglChooseConfig(display, alpha_attribute_list, NULL, 0, &count) || count <= 0) + return NULL; + EGLConfig *configs = malloc(count * sizeof(EGLConfig)); + EGLSurface surface = EGL_NO_SURFACE; + if (configs && p_eglChooseConfig(display, alpha_attribute_list, configs, count, &count)) { + for (EGLint i = 0; i < count && surface == EGL_NO_SURFACE; i++) { + EGLint visual = 0; + if (p_eglGetConfigAttrib(display, configs[i], EGL_NATIVE_VISUAL_ID, &visual) && + (unsigned long) visual == visual_id) + surface = p_eglCreateWindowSurface(display, configs[i], + (EGLNativeWindowType) native_window, NULL); + } + } + free(configs); + return surface == EGL_NO_SURFACE ? NULL : (CGLWindowRef) surface; +} + CGL_EXPORT void CGLDestroyWindow(CGLWindowRef window) { - eglDestroySurface(display, (EGLSurface) window); + /* CGLGetWindow returns NULL when eglCreateWindowSurface fails. */ + if (window == NULL) + return; + p_eglDestroySurface(display, (EGLSurface) window); } CGL_EXPORT CGLError CGLContextMakeCurrentAndAttachToWindow(CGLContextObj context, CGLWindowRef window) { if (!context) return kCGLBadContext; + EGLSurface oldSurface = context->egl_surface; context->egl_surface = (EGLSurface) window; - return CGLSetCurrentContext(context); + CGLError result = CGLSetCurrentContext(context); + if (result != kCGLNoError) + context->egl_surface = oldSurface; + return result; } static pthread_key_t current_context_key; @@ -198,18 +375,42 @@ CGLContextObj CGLGetCurrentContext(void) { return pthread_getspecific(key); } +/* darling-lol M57: CGLGetVirtualScreen/CGLSetVirtualScreen are used by + * Chromium's CGL backend; Darling's OpenGL did not provide them (lazy-bind + * abort: "Symbol not found: _CGLGetVirtualScreen"). A single virtual screen. */ +CGLError CGLGetVirtualScreen(CGLContextObj context, GLint *screen) { + if (!context) return kCGLBadContext; + if (!screen) return kCGLBadAddress; + *screen = 0; + return kCGLNoError; +} + +CGLError CGLSetVirtualScreen(CGLContextObj context, GLint screen) { + if (!context) return kCGLBadContext; + return screen == 0 ? kCGLNoError : kCGLBadValue; +} + CGLError CGLSetCurrentContext(CGLContextObj context) { + ensure_native_egl(); pthread_key_t key = get_current_context_key(); - pthread_setspecific(key, context); + CGLContextObj old = pthread_getspecific(key); + EGLDisplay currentDisplay = context ? context->egl_display : old ? old->egl_display : display; + if (!p_eglMakeCurrent) + return kCGLBadConnection; + if (!context && !old && !currentDisplay) + return kCGLNoError; + EGLBoolean success; if (context != NULL) { EGLSurface surface = context->egl_surface; - eglMakeCurrent(display, surface, surface, context->egl_context); + success = p_eglMakeCurrent(currentDisplay, surface, surface, context->egl_context); } else { - eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + success = p_eglMakeCurrent(currentDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); } - - return kCGLNoError; // FIXME + if (!success) + return kCGLBadState; + pthread_setspecific(key, context); + return kCGLNoError; } CGLError CGLSetFullScreen(CGLContextObj ctx) { @@ -223,11 +424,13 @@ CGLError CGLChoosePixelFormat( CGLPixelFormatObj *result, GLint *number_of_screens ) { + if (!attrs || !result || !number_of_screens) + return kCGLBadAddress; CGLPixelFormatObj format = malloc(sizeof(struct _CGLPixelFormatObj)); int count = attributes_count(attrs); format->retain_count = 1; - format->attributes = malloc(sizeof(CGLPixelFormatAttribute) * count); + format->attributes = calloc(count + 1, sizeof(CGLPixelFormatAttribute)); for (int i = 0; i < count; i++) { format->attributes[i] = attrs[i]; } @@ -251,6 +454,10 @@ CGLError CGLDescribePixelFormat( CGLPixelFormatAttribute attr, GLint *value ) { + if (!format) return kCGLBadPixelFormat; + if (!value) return kCGLBadAddress; + if (sreen_num != 0) return kCGLBadValue; + if (attr == kCGLPFAVirtualScreenCount) { *value = 1; return kCGLNoError; } for (int i = 0; format->attributes[i] != 0; i++) { int has_arg = attribute_has_argument(format->attributes[i]); @@ -303,37 +510,99 @@ GLuint CGLGetPixelFormatRetainCount(CGLPixelFormatObj pixelFormat) { return pixelFormat->retain_count; } -CGLError CGLCreateContext(CGLPixelFormatObj pixelFormat, CGLContextObj share, CGLContextObj *resultp) { +static CGLError createContext(CGLPixelFormatObj pixelFormat, CGLContextObj share, + EGLConfig windowConfig, CGLContextObj *resultp) { + if (!resultp) + return kCGLBadAddress; + *resultp = NULL; + ensureDefaultDisplay(); + /* eglBindAPI is thread-local, even when the display is already initialized. */ + if (!display || !config || !p_eglBindAPI || !p_eglBindAPI(EGL_OPENGL_API)) + return kCGLBadConnection; EGLContext egl_share = EGL_NO_CONTEXT; + EGLDisplay contextDisplay = share ? share->egl_display : display; + EGLConfig contextConfig = share ? share->egl_config : windowConfig ? windowConfig : config; if (share != NULL) { egl_share = share->egl_context; } - EGLContext egl_context = eglCreateContext(display, config, egl_share, NULL); + GLint profile = 0; + if (pixelFormat) + CGLDescribePixelFormat(pixelFormat, 0, kCGLPFAOpenGLProfile, &profile); + EGLint contextAttributes[] = { + 0x3098 /* EGL_CONTEXT_MAJOR_VERSION */, 3, + 0x30FB /* EGL_CONTEXT_MINOR_VERSION */, 2, + 0x30FD /* EGL_CONTEXT_OPENGL_PROFILE_MASK */, 1 /* CORE_PROFILE_BIT */, + EGL_NONE + }; + if (profile == 0x4100) { + contextAttributes[1] = 4; + contextAttributes[3] = 1; + } else if (profile && profile != 0x1000 && profile != 0x3200) { + return kCGLBadValue; + } + EGLContext egl_context = p_eglCreateContext(contextDisplay, contextConfig, egl_share, + profile == 0x3200 || profile == 0x4100 ? contextAttributes : NULL); if (egl_context == EGL_NO_CONTEXT) { + fprintf(stderr, "M58DG CGLCreateContext FAILED: display=%p config=%p share=%p p_eglCreateContext=%p\n", + (void*) display, (void*) config, (void*) egl_share, (void*) p_eglCreateContext); return kCGLBadContext; } + { + static int m58dg_ok = 0; + if (m58dg_ok++ < 5) + fprintf(stderr, "M58DG CGLCreateContext OK display=%p config=%p ctx=%p\n", + (void*) display, (void*) config, (void*) egl_context); + } + CGLContextObj context = malloc(sizeof(struct _CGLContextObj)); context->retain_count = 1; pthread_mutex_init(&(context->lock), NULL); context->egl_context = egl_context; + context->egl_display = contextDisplay; + context->egl_config = contextConfig; context->egl_surface = NULL; context->swap_interval = 1; + context->swap_interval_requested = 0; + context->native_present_surface=EGL_NO_SURFACE; + context->native_present_context=EGL_NO_CONTEXT; + context->native_present_target=NULL; + context->native_present_window=0; + context->native_next_present_ns=0; + context->native_async=NULL; *resultp = context; return kCGLNoError; } +CGLError CGLCreateContext(CGLPixelFormatObj pixelFormat, CGLContextObj share, CGLContextObj *resultp) { + /* Cocotron window compositing requests the default format with NULL. */ + return createContext(pixelFormat, share, NULL, resultp); +} + +CGLError CGLCreateContextForWindow(CGLWindowRef window, CGLContextObj *resultp) { + EGLint id = 0, count = 0; + EGLConfig windowConfig = NULL; + ensureDefaultDisplay(); + if (window && display && p_eglQuerySurface && + p_eglQuerySurface(display, (EGLSurface) window, EGL_CONFIG_ID, &id)) { + EGLint const attributes[] = {EGL_CONFIG_ID, id, EGL_NONE}; + if (!p_eglChooseConfig(display, attributes, &windowConfig, 1, &count)) + count = 0; + } + return createContext(NULL, NULL, count == 1 ? windowConfig : NULL, resultp); +} + CGLContextObj CGLRetainContext(CGLContextObj context) { if (context == NULL) { return NULL; } - context->retain_count++; + __sync_add_and_fetch(&context->retain_count, 1); return context; } @@ -342,11 +611,17 @@ void CGLReleaseContext(CGLContextObj context) { return; } - context->retain_count--; - - if (context->retain_count != 0) { + pthread_mutex_lock(&ioSurfaceBindingMutex); + if (__sync_sub_and_fetch(&context->retain_count, 1) != 0) { + pthread_mutex_unlock(&ioSurfaceBindingMutex); return; } + for (unsigned i = 0; i < ioSurfaceBindingCount; ) { + if (ioSurfaceBindings[i].context == context) + ioSurfaceBindings[i] = ioSurfaceBindings[--ioSurfaceBindingCount]; + else ++i; + } + pthread_mutex_unlock(&ioSurfaceBindingMutex); if (CGLGetCurrentContext() == context) { CGLSetCurrentContext(NULL); @@ -354,7 +629,11 @@ void CGLReleaseContext(CGLContextObj context) { pthread_mutex_destroy(&(context->lock)); - eglDestroyContext(display, context->egl_context); + stopAsyncPresent(context); + if(context->native_present_context!=EGL_NO_CONTEXT)p_eglDestroyContext(context->egl_display,context->native_present_context); + if(context->native_present_surface!=EGL_NO_SURFACE)p_eglDestroySurface(context->egl_display,context->native_present_surface); + if(context->native_present_target)__lolsys_syscall((long)context->native_present_target,sizeof(DarlingNativePresentTarget),0,0,0,0,11); + p_eglDestroyContext(context->egl_display, context->egl_context); free(context); } @@ -384,8 +663,325 @@ CGLError CGLUnlockContext(CGLContextObj context) { } CGLError CGLFlushDrawable(CGLContextObj context) { - eglSwapBuffers(display,context->egl_surface); - return kCGLNoError; + if (!context) + return kCGLBadContext; + /* darling-lol: EGL defaults to a swap interval of 1. When the window is covered or the compositor throttles it + * (Xwayland answers about once a second), every swap blocks that long and the game's frame rate collapses to ~1 fps. + * macOS apps get an unthrottled flush unless they ask for vsync through kCGLCPSwapInterval, which is handled in + * CGLSetParameter; apply "no vsync" to unconfigured contexts while current. */ + if (!context->swap_interval_requested && p_eglSwapInterval && CGLGetCurrentContext() == context) { + p_eglSwapInterval(context->egl_display, 0); + } + return p_eglSwapBuffers(context->egl_display, context->egl_surface) ? kCGLNoError : kCGLBadState; +} + +static GLenum textureBindingEnum(GLenum target) { + switch (target) { + case GL_TEXTURE_2D: return GL_TEXTURE_BINDING_2D; +#ifdef GL_TEXTURE_RECTANGLE + case GL_TEXTURE_RECTANGLE: return GL_TEXTURE_BINDING_RECTANGLE; +#endif + default: return 0; + } +} + +/* macOS exposes IOSurfaces to CGL as texture storage. Mesa/EGL does not have + * that CGL entry point, but Chromium only needs the pixels to reach the bound + * texture. Upload from the shared IOSurface mapping synchronously; this also + * preserves the cross-process IOSurface transport implemented by our shim. */ +static uint64_t nativeMonotonicNS(void); +static uint32_t nativeUploadCount, nativeUploadMicroseconds; +CGLError CGLTexImageIOSurface2D(CGLContextObj context, GLenum target, + GLenum internalFormat, GLsizei width, GLsizei height, GLenum format, + GLenum type, IOSurfaceRef ioSurface, GLuint plane) +{ + if (!context) + return kCGLBadContext; + if (!ioSurface) + return kCGLBadAddress; + + void *pixels = IOSurfaceGetBaseAddressOfPlane(ioSurface, plane); + size_t bytesPerRow = IOSurfaceGetBytesPerRowOfPlane(ioSurface, plane); + size_t bytesPerElement = IOSurfaceGetBytesPerElementOfPlane(ioSurface, plane); + if (!pixels || !bytesPerRow || !bytesPerElement) + return kCGLBadValue; + + CGLContextObj oldContext = CGLGetCurrentContext(); + if (oldContext != context) { + CGLError result = CGLSetCurrentContext(context); + if (result != kCGLNoError) + return result; + } + + uint64_t uploadStarted = nativeMonotonicNS(); + GLint oldRowLength = 0; + GLint oldAlignment = 0; + GLint boundTexture = 0; + /* ANGLE can leave an unrelated probe error pending before calling this + * CGL API. CGLTexImageIOSurface2D reports only its own operation. */ + while (glGetError() != GL_NO_ERROR) {} + glGetIntegerv(GL_UNPACK_ROW_LENGTH, &oldRowLength); + glGetIntegerv(GL_UNPACK_ALIGNMENT, &oldAlignment); + GLenum bindingEnum = textureBindingEnum(target); + if (bindingEnum) + glGetIntegerv(bindingEnum, &boundTexture); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glPixelStorei(GL_UNPACK_ROW_LENGTH, (GLint)(bytesPerRow / bytesPerElement)); + GLint allocatedWidth=0,allocatedHeight=0,allocatedFormat=0; + glGetTexLevelParameteriv(target,0,GL_TEXTURE_WIDTH,&allocatedWidth); + glGetTexLevelParameteriv(target,0,GL_TEXTURE_HEIGHT,&allocatedHeight); + glGetTexLevelParameteriv(target,0,GL_TEXTURE_INTERNAL_FORMAT,&allocatedFormat); + if(allocatedWidth==width && allocatedHeight==height && allocatedFormat==(GLint)internalFormat) + glTexSubImage2D(target,0,0,0,width,height,format,type,pixels); + else + glTexImage2D(target, 0, (GLint)internalFormat, width, height, 0, + format, type, pixels); + GLenum error = glGetError(); + glPixelStorei(GL_UNPACK_ROW_LENGTH, oldRowLength); + glPixelStorei(GL_UNPACK_ALIGNMENT, oldAlignment); + + if (error == GL_NO_ERROR && boundTexture != 0) { + CGLIOSurfaceBinding binding = { + IOSurfaceGetID(ioSurface), context, target, format, type, + (GLuint)boundTexture, plane, ioSurface + }; + pthread_mutex_lock(&ioSurfaceBindingMutex); + unsigned slot = ioSurfaceBindingCount; + for (unsigned i = 0; i < ioSurfaceBindingCount; ++i) { + if (ioSurfaceBindings[i].surfaceID == binding.surfaceID) { + slot = i; + break; + } + } + if (slot == ioSurfaceBindingCapacity) { + unsigned capacity = ioSurfaceBindingCapacity * 2; + CGLIOSurfaceBinding *larger = malloc(capacity * sizeof(*larger)); + if (larger) { + memcpy(larger, ioSurfaceBindings, ioSurfaceBindingCount * sizeof(*larger)); + if (ioSurfaceBindings != initialSurfaceBindings) free(ioSurfaceBindings); + ioSurfaceBindings = larger; + ioSurfaceBindingCapacity = capacity; + } + } + if (slot < ioSurfaceBindingCapacity) { + ioSurfaceBindings[slot] = binding; + if (slot == ioSurfaceBindingCount) ++ioSurfaceBindingCount; + } else error = GL_OUT_OF_MEMORY; + pthread_mutex_unlock(&ioSurfaceBindingMutex); + } + + CGLError restore = kCGLNoError; + if (oldContext != context) + restore = CGLSetCurrentContext(oldContext); + if (error != GL_NO_ERROR) + fprintf(stderr, "CGLTexImageIOSurface2D glTexImage2D error=0x%x target=0x%x format=0x%x type=0x%x %dx%d\n", + error, target, format, type, width, height); + __atomic_fetch_add(&nativeUploadCount,1,__ATOMIC_RELAXED); + __atomic_fetch_add(&nativeUploadMicroseconds,(uint32_t)((nativeMonotonicNS()-uploadStarted)/1000),__ATOMIC_RELAXED); + return error == GL_NO_ERROR ? restore : kCGLBadState; +} + +/* Called before IOSurface storage is destroyed; no owning surface reference. */ +void CGLForgetIOSurface(IOSurfaceRef surface) { + pthread_mutex_lock(&ioSurfaceBindingMutex); + for (unsigned i = 0; i < ioSurfaceBindingCount; ) { + if (ioSurfaceBindings[i].surface == surface) + ioSurfaceBindings[i] = ioSurfaceBindings[--ioSurfaceBindingCount]; + else ++i; + } + pthread_mutex_unlock(&ioSurfaceBindingMutex); +} + +#include "CGLAsyncPresent.inc" + +/* Present a flattened compositor texture directly. The native transport + * acknowledges suspension of CPU writes before the first GL swap. Fallback + * leaves the existing readback path available and restores all GL bindings. */ +CGL_EXPORT int CGLPresentIOSurface(IOSurfaceRef surface) { + if(!surface || !getenv("RCLOL_GPU_DIRECT_PRESENT") || IOSurfaceGetPlaneCount(surface)>1 || + IOSurfaceGetBytesPerElement(surface)!=4)return 0; + size_t width=IOSurfaceGetWidth(surface),height=IOSurfaceGetHeight(surface); + if(width<256 || height<256)return 0; + CGLIOSurfaceBinding binding={0};IOSurfaceID id=IOSurfaceGetID(surface); + pthread_mutex_lock(&ioSurfaceBindingMutex); + for(unsigned i=0;inative_present_target){ + char path[128];snprintf(path,sizeof(path),"/dev/shm/darling-ca-gpu-target-%u-%zu-%zu",getuid(),width,height); + int fd=(int)__lolsys_syscall((long)path,2,0,0,0,0,2);if(getenv("RCLOL_GPU_DIRECT_TRACE"))fprintf(stderr,"GPU_DIRECT target_open=%d uid=%u size=%zux%zu\n",fd,getuid(),width,height);if(fd>=0){ + struct stat metadata; + long mapped=-1; + if(fstat(fd,&metadata)==0 && metadata.st_size>=sizeof(DarlingNativePresentTarget) && metadata.st_uid==getuid()) + mapped=__lolsys_syscall(0,sizeof(DarlingNativePresentTarget),3,1,fd,0,9); + if(mapped>0 || mapped < -4095)ctx->native_present_target=(void*)mapped;__lolsys_syscall(fd,0,0,0,0,0,3); + } + } + DarlingNativePresentTarget *target=ctx->native_present_target; + /* A Chromium context renders several windows. An unrelated surface must + * use readback without stopping the active window's async presenter. */ + if(target && __atomic_load_n(&target->magic,__ATOMIC_ACQUIRE)==DARLING_NATIVE_PRESENT_MAGIC && + target->target && (target->width!=width || target->height!=height)) { + CGLReleaseContext(ctx);return 0; + } + if(!target || __atomic_load_n(&target->magic,__ATOMIC_ACQUIRE)!=DARLING_NATIVE_PRESENT_MAGIC || + !target->target){ + stopAsyncPresent(ctx); + if(target){__lolsys_syscall((long)target,sizeof(DarlingNativePresentTarget),0,0,0,0,11);ctx->native_present_target=NULL;ctx->native_present_window=0;} + CGLReleaseContext(ctx);return 0; + } + if(ctx->native_present_window!=target->target){ + stopAsyncPresent(ctx); + if(ctx->native_present_context!=EGL_NO_CONTEXT)p_eglDestroyContext(ctx->egl_display,ctx->native_present_context); + if(ctx->native_present_surface!=EGL_NO_SURFACE)p_eglDestroySurface(ctx->egl_display,ctx->native_present_surface); + ctx->native_present_surface=EGL_NO_SURFACE;ctx->native_present_context=EGL_NO_CONTEXT; + EGLint attrs[]={EGL_SURFACE_TYPE,EGL_WINDOW_BIT,EGL_RENDERABLE_TYPE,EGL_OPENGL_BIT,EGL_NONE}; + EGLConfig *candidates=NULL,cfg=NULL;EGLint count=0,selectedVisual=0; + if(p_eglChooseConfig(ctx->egl_display,attrs,NULL,0,&count) && count>0 && count<=65536) + candidates=calloc(count,sizeof(*candidates)); + if(candidates && p_eglChooseConfig(ctx->egl_display,attrs,candidates,count,&count) && p_eglGetConfigAttrib){ + for(EGLint n=0;negl_display,candidates[n],EGL_NATIVE_VISUAL_ID,&visual) && visual==(EGLint)target->visual){cfg=candidates[n];selectedVisual=visual;break;} + } + } + free(candidates); + if(cfg){ + ctx->native_present_surface=p_eglCreateWindowSurface(ctx->egl_display,cfg,(EGLNativeWindowType)(uintptr_t)target->target,NULL); + EGLint contextAttrs[]={0x3098,3,0x30FB,2,0x30FD,1,EGL_NONE}; + ctx->native_present_context=p_eglCreateContext(ctx->egl_display,cfg,ctx->egl_context,contextAttrs); + } + if(getenv("RCLOL_GPU_DIRECT_TRACE"))fprintf(stderr,"GPU_DIRECT selected_visual=%d requested_visual=%u\n",selectedVisual,target->visual); + EGLint actualWidth=0,actualHeight=0; + if(ctx->native_present_surface!=EGL_NO_SURFACE){p_eglQuerySurface(ctx->egl_display,ctx->native_present_surface,EGL_WIDTH,&actualWidth);p_eglQuerySurface(ctx->egl_display,ctx->native_present_surface,EGL_HEIGHT,&actualHeight);} + if(getenv("RCLOL_GPU_DIRECT_TRACE"))fprintf(stderr,"GPU_DIRECT native_size=%dx%d requested=%zux%zu\n",actualWidth,actualHeight,width,height); + ctx->native_present_window=target->target; + if(getenv("RCLOL_GPU_DIRECT_TRACE"))fprintf(stderr,"GPU_DIRECT surface_created=%d shared_context=%d visual=%u target=%u\n",ctx->native_present_surface!=EGL_NO_SURFACE,ctx->native_present_context!=EGL_NO_CONTEXT,target->visual,target->target); + } + if(ctx->native_present_surface==EGL_NO_SURFACE || ctx->native_present_context==EGL_NO_CONTEXT){CGLReleaseContext(ctx);return 0;} + uint32_t ready=__atomic_load_n(&target->ready,__ATOMIC_ACQUIRE); + if(!ready){__atomic_store_n(&target->ready,1,__ATOMIC_RELEASE); + for(unsigned n=0;n<40 && __atomic_load_n(&target->ready,__ATOMIC_ACQUIRE)==1;++n)usleep(500); + if(__atomic_load_n(&target->ready,__ATOMIC_ACQUIRE)!=2){__atomic_store_n(&target->ready,0,__ATOMIC_RELEASE);CGLReleaseContext(ctx);return 0;} + } + /* Bound the producer independently of async display cadence when testing + * unthrottled begin frames. Keep waits on the owning GPU thread and use + * absolute monotonic deadlines; async staging remains bounded to three. */ + if(getenv("RCLOL_GPU_DIRECT_PACE")){ + struct timespec now; + if(__lolsys_syscall(1,(long)&now,0,0,0,0,228)==0){ + uint64_t current=(uint64_t)now.tv_sec*1000000000ull+now.tv_nsec; + uint64_t deadline=ctx->native_next_present_ns; + uint64_t paceNS=nativeRefreshIntervalNS(); + if(!deadline || (current>deadline && current-deadline>paceNS))deadline=current; + if(deadline>current){ + struct timespec wake={deadline/1000000000ull,deadline%1000000000ull}; + while(__lolsys_syscall(1,1,(long)&wake,0,0,0,230)==-4){} + } + ctx->native_next_present_ns=deadline+paceNS; + } + } + if(getenv("RCLOL_GPU_DIRECT_ASYNC")){int queued=enqueueAsyncPresent(ctx,binding.texture,binding.target,width,height);if(!queued){stopAsyncPresent(ctx);__atomic_store_n(&target->ready,0,__ATOMIC_RELEASE);}CGLReleaseContext(ctx);return queued;} + GLsync completion=glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE,0);glFlush(); + int ok=0;GLsync presented=NULL; + if(completion && p_eglMakeCurrent(ctx->egl_display,ctx->native_present_surface,ctx->native_present_surface,ctx->native_present_context)){ + glWaitSync(completion,0,GL_TIMEOUT_IGNORED);glDeleteSync(completion);completion=NULL; + p_eglSwapInterval(ctx->egl_display,0); + GLuint fbo=0;glGenFramebuffers(1,&fbo);glBindFramebuffer(GL_READ_FRAMEBUFFER,fbo); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,binding.target,binding.texture,0); + if(glCheckFramebufferStatus(GL_READ_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE){ + glBindFramebuffer(GL_DRAW_FRAMEBUFFER,0);glDrawBuffer(GL_BACK);glReadBuffer(GL_COLOR_ATTACHMENT0); + glDisable(GL_SCISSOR_TEST);glDisable(GL_FRAMEBUFFER_SRGB); + glBlitFramebuffer(0,height,width,0,0,0,width,height,GL_COLOR_BUFFER_BIT,GL_NEAREST); + /* The producer fence and EGL swap order GPU work without a CPU wait. */ + static unsigned syntheticSamples; + int inspect=target->flags==0x54455354 && getenv("RCLOL_GPU_DIRECT_SYNTHETIC_TEST") && syntheticSamples++==10; + if(inspect){ + unsigned char rgba[4];glReadPixels(0,0,1,1,GL_RGBA,GL_UNSIGNED_BYTE,rgba); + fprintf(stderr,"DIRECT_SHARED_SOURCE rgba=%u,%u,%u,%u\n",rgba[0],rgba[1],rgba[2],rgba[3]); + glBindFramebuffer(GL_READ_FRAMEBUFFER,0);glReadBuffer(GL_BACK);glReadPixels(0,0,1,1,GL_RGBA,GL_UNSIGNED_BYTE,rgba); + fprintf(stderr,"DIRECT_SHARED_BACK rgba=%u,%u,%u,%u\n",rgba[0],rgba[1],rgba[2],rgba[3]); + } + presented=glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE,0);glFlush(); + if(presented && glGetError()==GL_NO_ERROR){ok=p_eglSwapBuffers(ctx->egl_display,ctx->native_present_surface); + static unsigned traceCount; + if(getenv("RCLOL_GPU_DIRECT_TRACE") && traceCount++<3)fprintf(stderr,"GPU_DIRECT swap_result=%d swap_fn=%p surface=%p\n",ok,(void*)p_eglSwapBuffers,(void*)ctx->native_present_surface); + } + if(inspect){unsigned char rgba[4];glReadBuffer(GL_FRONT);glReadPixels(0,0,1,1,GL_RGBA,GL_UNSIGNED_BYTE,rgba); + fprintf(stderr,"DIRECT_SHARED_FRONT rgba=%u,%u,%u,%u error=%u\n",rgba[0],rgba[1],rgba[2],rgba[3],glGetError());} + } + glBindFramebuffer(GL_READ_FRAMEBUFFER,0);glDeleteFramebuffers(1,&fbo); + if(!p_eglMakeCurrent(ctx->egl_display,ctx->egl_surface,ctx->egl_surface,ctx->egl_context))ok=0; + else if(presented){glWaitSync(presented,0,GL_TIMEOUT_IGNORED);glDeleteSync(presented);presented=NULL;} + if(presented)glDeleteSync(presented); + } + if(completion)glDeleteSync(completion); + __atomic_store_n(&target->ready,ok?3:0,__ATOMIC_RELEASE); + if(ok){__atomic_add_fetch(&target->sequence,1,__ATOMIC_RELEASE);target->presenter=(uint32_t)getpid();} + CGLReleaseContext(ctx);return ok; +} + +CGLError CGLReadbackIOSurface(IOSurfaceRef ioSurface) { + if (!ioSurface) + return kCGLBadAddress; + IOSurfaceID surfaceID = IOSurfaceGetID(ioSurface); + CGLIOSurfaceBinding binding = {0}; + pthread_mutex_lock(&ioSurfaceBindingMutex); + for (unsigned i = 0; i < ioSurfaceBindingCount; ++i) { + if (ioSurfaceBindings[i].surfaceID == surfaceID) { + binding = ioSurfaceBindings[i]; + CGLRetainContext(binding.context); + break; + } + } + pthread_mutex_unlock(&ioSurfaceBindingMutex); + if (!binding.context || !binding.texture) + return kCGLBadValue; + + void *pixels = IOSurfaceGetBaseAddressOfPlane(ioSurface, binding.plane); + size_t bytesPerRow = IOSurfaceGetBytesPerRowOfPlane(ioSurface, binding.plane); + size_t bytesPerElement = IOSurfaceGetBytesPerElementOfPlane(ioSurface, binding.plane); + GLenum bindingEnum = textureBindingEnum(binding.target); + if (!pixels || !bytesPerRow || !bytesPerElement || !bindingEnum) { + CGLReleaseContext(binding.context); + return kCGLBadValue; + } + + CGLContextObj oldContext = CGLGetCurrentContext(); + if (oldContext != binding.context) { + CGLError result = CGLSetCurrentContext(binding.context); + if (result != kCGLNoError) { + CGLReleaseContext(binding.context); + return result; + } + } + GLint oldTexture = 0, oldRowLength = 0, oldAlignment = 0; + while (glGetError() != GL_NO_ERROR) {} + glGetIntegerv(bindingEnum, &oldTexture); + glGetIntegerv(GL_PACK_ROW_LENGTH, &oldRowLength); + glGetIntegerv(GL_PACK_ALIGNMENT, &oldAlignment); + glBindTexture(binding.target, binding.texture); + glFinish(); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glPixelStorei(GL_PACK_ROW_LENGTH, (GLint)(bytesPerRow / bytesPerElement)); + IOSurfaceLock(ioSurface, 0, NULL); + glGetTexImage(binding.target, 0, binding.format, binding.type, pixels); + GLenum error = glGetError(); + typedef kern_return_t (*GPUUnlockFn)(IOSurfaceRef); + GPUUnlockFn gpuUnlock = (GPUUnlockFn)dlsym(RTLD_DEFAULT, "_IOSurfaceUnlockGPUReadback"); + if (gpuUnlock) gpuUnlock(ioSurface); + else IOSurfaceUnlock(ioSurface, 0, NULL); + glPixelStorei(GL_PACK_ROW_LENGTH, oldRowLength); + glPixelStorei(GL_PACK_ALIGNMENT, oldAlignment); + glBindTexture(binding.target, (GLuint)oldTexture); + CGLError restore = kCGLNoError; + if (oldContext != binding.context) + restore = CGLSetCurrentContext(oldContext); + CGLReleaseContext(binding.context); + return error == GL_NO_ERROR ? restore : kCGLBadState; } CGLError CGLSetParameter(CGLContextObj context, CGLContextParameter parameter, const GLint *value) { @@ -395,9 +991,11 @@ CGLError CGLSetParameter(CGLContextObj context, CGLContextParameter parameter, c if (parameter == kCGLCPSwapInterval) { GLint v = *value; - EGLBoolean success = eglSwapInterval(display, v); - if (success) + EGLBoolean success = p_eglSwapInterval(display, v); + if (success) { context->swap_interval = v; + context->swap_interval_requested = 1; + } return success ? kCGLNoError : kCGLBadValue; } fprintf(stderr, "CGLSetParameter unimplemented for parameter %d\n", parameter); @@ -417,14 +1015,19 @@ CGLError CGLGetParameter(CGLContextObj context, CGLContextParameter parameter, G return kCGLNoError; } -CGLError CGLDescribeRenderer(CGLRendererInfoObj rend, long rend_num, CGLRendererProperty prop, long *value) { - return kCGLNoError; +CGLError CGLDescribeRenderer(CGLRendererInfoObj rend, GLint rend_num, CGLRendererProperty prop, GLint *value) { + if (!value) return kCGLBadAddress; + *value = 0; + return kCGLBadRendererInfo; } -CGLError CGLQueryRendererInfo(unsigned long display_mask, CGLRendererInfoObj *rend, long *nrend) { - return kCGLNoError; +CGLError CGLQueryRendererInfo(GLuint display_mask, CGLRendererInfoObj *rend, GLint *nrend) { + if (!rend || !nrend) return kCGLBadAddress; + *rend = NULL; + *nrend = 0; + return kCGLBadRendererInfo; } CGLError CGLDestroyRendererInfo(CGLRendererInfoObj rend) { return kCGLNoError; -} \ No newline at end of file +} diff --git a/src/frameworks/OpenGL/include/OpenGL/CGLInternal.h b/src/frameworks/OpenGL/include/OpenGL/CGLInternal.h index 1d0b10e65..5cdd9a89c 100644 --- a/src/frameworks/OpenGL/include/OpenGL/CGLInternal.h +++ b/src/frameworks/OpenGL/include/OpenGL/CGLInternal.h @@ -14,8 +14,12 @@ typedef void *CGLWindowRef; CGL_EXPORT CGLError CGLRegisterNativeDisplay(void *native_display); CGL_EXPORT CGLWindowRef CGLGetWindow(void *native_window); +// A surface with an alpha channel, for a window created with the given 32-bit visual. +CGL_EXPORT CGLWindowRef CGLGetWindowWithAlpha(void *native_window, unsigned long visual_id); CGL_EXPORT void CGLDestroyWindow(CGLWindowRef window); +// A context that can be made current on the window: one created from its surface's config. +CGL_EXPORT CGLError CGLCreateContextForWindow(CGLWindowRef window, CGLContextObj *result); CGL_EXPORT CGLError CGLContextMakeCurrentAndAttachToWindow(CGLContextObj context, CGLWindowRef window); CGLError CGLSetSurface(CGLContextObj gl, CGSConnectionID cid, CGSWindowID wid, CGSSurfaceID sid); CGLContextObj CGWindowContextCreate(CGSConnectionID cid, CGSWindowID wid, CFDictionaryRef options); diff --git a/src/frameworks/OpenGL/include/OpenGL/CGLTypes.h b/src/frameworks/OpenGL/include/OpenGL/CGLTypes.h index 7d5eda01a..803ac02d3 100644 --- a/src/frameworks/OpenGL/include/OpenGL/CGLTypes.h +++ b/src/frameworks/OpenGL/include/OpenGL/CGLTypes.h @@ -64,6 +64,7 @@ enum { kCGLPFARemotePBuffer = 91, kCGLPFAAllowOfflineRenderers = 96, kCGLPFAAcceleratedCompute = 97, + kCGLPFAOpenGLProfile = 99, kCGLPFAVirtualScreenCount = 128, }; diff --git a/src/frameworks/OpenGL/include/OpenGL/OpenGL.h b/src/frameworks/OpenGL/include/OpenGL/OpenGL.h index a700038d7..7bb98bf4b 100644 --- a/src/frameworks/OpenGL/include/OpenGL/OpenGL.h +++ b/src/frameworks/OpenGL/include/OpenGL/OpenGL.h @@ -8,6 +8,7 @@ #endif #include +#include #ifdef __cplusplus extern "C" { @@ -27,6 +28,8 @@ CGL_EXPORT CGLError CGLUnlockContext(CGLContextObj context); CGL_EXPORT CGLError CGLSetCurrentContext(CGLContextObj ctx); CGL_EXPORT CGLError CGLSetFullScreen(CGLContextObj ctx); CGL_EXPORT CGLContextObj CGLGetCurrentContext(void); +CGL_EXPORT CGLError CGLGetVirtualScreen(CGLContextObj ctx, GLint *screen); +CGL_EXPORT CGLError CGLSetVirtualScreen(CGLContextObj ctx, GLint screen); CGL_EXPORT CGLError CGLSetParameter(CGLContextObj context, CGLContextParameter parameter, const GLint *value); CGL_EXPORT CGLError CGLGetParameter(CGLContextObj context, CGLContextParameter parameter, GLint *value); @@ -40,9 +43,9 @@ CGL_EXPORT CGLError CGLDestroyPixelFormat(CGLPixelFormatObj pixelFormat); CGL_EXPORT GLuint CGLGetPixelFormatRetainCount(CGLPixelFormatObj pixelFormat); CGL_EXPORT CGLError CGLDescribePixelFormat(CGLPixelFormatObj pixelFormat, GLint screenNumber, CGLPixelFormatAttribute attribute, GLint *valuep); -CGL_EXPORT CGLError CGLQueryRendererInfo(unsigned long display_mask, CGLRendererInfoObj *rend, long *nrend); +CGL_EXPORT CGLError CGLQueryRendererInfo(GLuint display_mask, CGLRendererInfoObj *rend, GLint *nrend); CGL_EXPORT CGLError CGLDestroyRendererInfo(CGLRendererInfoObj rend); -CGL_EXPORT CGLError CGLDescribeRenderer(CGLRendererInfoObj rend, long rend_num, CGLRendererProperty prop, long *value); +CGL_EXPORT CGLError CGLDescribeRenderer(CGLRendererInfoObj rend, GLint rend_num, CGLRendererProperty prop, GLint *value); CGL_EXPORT CGLError CGLCreatePBuffer(GLsizei width, GLsizei height, GLenum target, GLenum internalFormat, GLint maxDetail, CGLPBufferObj *pbuffer); CGL_EXPORT CGLError CGLDescribePBuffer(CGLPBufferObj pbuffer, GLsizei *width, GLsizei *height, GLenum *target, GLenum *internalFormat, GLint *mipmap); @@ -53,6 +56,13 @@ CGL_EXPORT CGLError CGLDestroyPBuffer(CGLPBufferObj pbuffer); CGL_EXPORT CGLError CGLGetPBuffer(CGLContextObj context, CGLPBufferObj *pbuffer, GLenum *face, GLint *level, GLint *screen); CGL_EXPORT CGLError CGLSetPBuffer(CGLContextObj context, CGLPBufferObj pbuffer, GLenum face, GLint level, GLint screen); CGL_EXPORT CGLError CGLTexImagePBuffer(CGLContextObj context, CGLPBufferObj pbuffer, GLenum sourceBuffer); +CGL_EXPORT CGLError CGLTexImageIOSurface2D(CGLContextObj context, GLenum target, + GLenum internalFormat, GLsizei width, GLsizei height, GLenum format, + GLenum type, IOSurfaceRef ioSurface, GLuint plane); +/* Darling extension used by QuartzCore's remote-layer fallback. Real CGL + * texture storage is bidirectionally backed by IOSurface; our Mesa fallback + * must explicitly copy rendered texture pixels back to the surface. */ +CGL_EXPORT CGLError CGLReadbackIOSurface(IOSurfaceRef ioSurface); #ifdef __cplusplus } diff --git a/src/frameworks/VideoToolbox/include/VideoToolbox/VTCompressionProperties.h b/src/frameworks/VideoToolbox/include/VideoToolbox/VTCompressionProperties.h index 68242ee34..a9108c9c1 100644 --- a/src/frameworks/VideoToolbox/include/VideoToolbox/VTCompressionProperties.h +++ b/src/frameworks/VideoToolbox/include/VideoToolbox/VTCompressionProperties.h @@ -34,4 +34,10 @@ extern CFStringRef const kVTCompressionPropertyKey_ExpectedFrameRate; extern CFStringRef const kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder; +extern CFStringRef const kVTCompressionPropertyKey_BaseLayerFrameRateFraction; +extern CFStringRef const kVTCompressionPropertyKey_ColorPrimaries; +extern CFStringRef const kVTCompressionPropertyKey_TransferFunction; +extern CFStringRef const kVTCompressionPropertyKey_YCbCrMatrix; +extern CFStringRef const kVTVideoEncoderSpecification_EnableLowLatencyRateControl; + #endif diff --git a/src/frameworks/VideoToolbox/src/VTCompressionProperties.c b/src/frameworks/VideoToolbox/src/VTCompressionProperties.c index 9e363904e..774557a64 100644 --- a/src/frameworks/VideoToolbox/src/VTCompressionProperties.c +++ b/src/frameworks/VideoToolbox/src/VTCompressionProperties.c @@ -30,3 +30,9 @@ const CFStringRef kVTCompressionPropertyKey_MaxFrameDelayCount = CFSTR("MaxFrame const CFStringRef kVTCompressionPropertyKey_ExpectedFrameRate = CFSTR("ExpectedFrameRate"); const CFStringRef kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder = CFSTR("RequireHardwareAcceleratedVideoEncoder"); + +const CFStringRef kVTCompressionPropertyKey_BaseLayerFrameRateFraction = CFSTR("BaseLayerFrameRateFraction"); +const CFStringRef kVTCompressionPropertyKey_ColorPrimaries = CFSTR("ColorPrimaries"); +const CFStringRef kVTCompressionPropertyKey_TransferFunction = CFSTR("TransferFunction"); +const CFStringRef kVTCompressionPropertyKey_YCbCrMatrix = CFSTR("YCbCrMatrix"); +const CFStringRef kVTVideoEncoderSpecification_EnableLowLatencyRateControl = CFSTR("EnableLowLatencyRateControl"); diff --git a/src/frameworks/Vision/include/Vision/VNDetectBarcodesRequest.h b/src/frameworks/Vision/include/Vision/VNDetectBarcodesRequest.h index 62d827aa2..15383581b 100644 --- a/src/frameworks/Vision/include/Vision/VNDetectBarcodesRequest.h +++ b/src/frameworks/Vision/include/Vision/VNDetectBarcodesRequest.h @@ -19,6 +19,26 @@ #include +typedef NSString *VNBarcodeSymbology NS_TYPED_ENUM; + +extern VNBarcodeSymbology const VNBarcodeSymbologyAztec; +extern VNBarcodeSymbology const VNBarcodeSymbologyCode128; +extern VNBarcodeSymbology const VNBarcodeSymbologyCode39; +extern VNBarcodeSymbology const VNBarcodeSymbologyCode39Checksum; +extern VNBarcodeSymbology const VNBarcodeSymbologyCode39FullASCII; +extern VNBarcodeSymbology const VNBarcodeSymbologyCode39FullASCIIChecksum; +extern VNBarcodeSymbology const VNBarcodeSymbologyCode93; +extern VNBarcodeSymbology const VNBarcodeSymbologyCode93i; +extern VNBarcodeSymbology const VNBarcodeSymbologyDataMatrix; +extern VNBarcodeSymbology const VNBarcodeSymbologyEAN13; +extern VNBarcodeSymbology const VNBarcodeSymbologyEAN8; +extern VNBarcodeSymbology const VNBarcodeSymbologyI2of5; +extern VNBarcodeSymbology const VNBarcodeSymbologyI2of5Checksum; +extern VNBarcodeSymbology const VNBarcodeSymbologyITF14; +extern VNBarcodeSymbology const VNBarcodeSymbologyPDF417; +extern VNBarcodeSymbology const VNBarcodeSymbologyQR; +extern VNBarcodeSymbology const VNBarcodeSymbologyUPCE; + @interface VNDetectBarcodesRequest : NSObject @end diff --git a/src/frameworks/Vision/src/VNDetectBarcodesRequest.m b/src/frameworks/Vision/src/VNDetectBarcodesRequest.m index 9a489b386..9fe56cc3f 100644 --- a/src/frameworks/Vision/src/VNDetectBarcodesRequest.m +++ b/src/frameworks/Vision/src/VNDetectBarcodesRequest.m @@ -19,6 +19,24 @@ #import +VNBarcodeSymbology const VNBarcodeSymbologyAztec = @"org.iso.Aztec"; +VNBarcodeSymbology const VNBarcodeSymbologyCode128 = @"org.iso.Code128"; +VNBarcodeSymbology const VNBarcodeSymbologyCode39 = @"org.iso.Code39"; +VNBarcodeSymbology const VNBarcodeSymbologyCode39Checksum = @"org.iso.Code39Checksum"; +VNBarcodeSymbology const VNBarcodeSymbologyCode39FullASCII = @"org.iso.Code39FullASCII"; +VNBarcodeSymbology const VNBarcodeSymbologyCode39FullASCIIChecksum = @"org.iso.Code39FullASCIIChecksum"; +VNBarcodeSymbology const VNBarcodeSymbologyCode93 = @"org.iso.Code93"; +VNBarcodeSymbology const VNBarcodeSymbologyCode93i = @"org.iso.Code93i"; +VNBarcodeSymbology const VNBarcodeSymbologyDataMatrix = @"org.iso.DataMatrix"; +VNBarcodeSymbology const VNBarcodeSymbologyEAN13 = @"org.gs1.EAN-13"; +VNBarcodeSymbology const VNBarcodeSymbologyEAN8 = @"org.gs1.EAN-8"; +VNBarcodeSymbology const VNBarcodeSymbologyI2of5 = @"org.iso.I2of5"; +VNBarcodeSymbology const VNBarcodeSymbologyI2of5Checksum = @"org.iso.I2of5Checksum"; +VNBarcodeSymbology const VNBarcodeSymbologyITF14 = @"org.iso.ITF14"; +VNBarcodeSymbology const VNBarcodeSymbologyPDF417 = @"org.iso.PDF417"; +VNBarcodeSymbology const VNBarcodeSymbologyQR = @"org.iso.QRCode"; +VNBarcodeSymbology const VNBarcodeSymbologyUPCE = @"org.gs1.UPC-E"; + @implementation VNDetectBarcodesRequest - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector diff --git a/src/libsandbox/src/sandbox.c b/src/libsandbox/src/sandbox.c index 5af5298aa..fb59ee5d1 100644 --- a/src/libsandbox/src/sandbox.c +++ b/src/libsandbox/src/sandbox.c @@ -21,6 +21,8 @@ #include #include #include +#include +#include static int verbose = 0; @@ -29,63 +31,105 @@ static void initme(void) { verbose = getenv("STUB_VERBOSE") != NULL; } -void* sandbox_apply(void) +/* darling-lol: SeatbeltExtensions token family. Chromium's seatbelt client + * issues/consumes/releases these around sandboxed child setup; without + * enforcement we hand out dummy nonzero tokens and accept everything. + * (These live in libsandbox.dylib on macOS, hence here and not in + * libsystem_sandbox.) */ +static int64_t sandbox_extension_next_token = 1; + +int64_t sandbox_extension_issue_file(const char *path, uint32_t flags) { - if (verbose) puts("STUB: sandbox_apply called"); - return NULL; + if (verbose) puts("STUB: sandbox_extension_issue_file called"); + (void)path; (void)flags; + return sandbox_extension_next_token++; } -void* sandbox_apply_container(void) +int64_t sandbox_extension_issue_mach(const char *name, uint32_t flags) { - if (verbose) puts("STUB: sandbox_apply_container called"); - return NULL; + if (verbose) puts("STUB: sandbox_extension_issue_mach called"); + (void)name; (void)flags; + return sandbox_extension_next_token++; } -void* sandbox_compile_entitlements(void) +int sandbox_extension_consume(int64_t token) { - if (verbose) puts("STUB: sandbox_compile_entitlements called"); - return NULL; + if (verbose) puts("STUB: sandbox_extension_consume called"); + (void)token; + return 0; } -void* sandbox_compile_file(void) +int sandbox_extension_release(int64_t token) +{ + if (verbose) puts("STUB: sandbox_extension_release called"); + (void)token; + return 0; +} + +/* darling-lol: compile/apply family. No seatbelt compiler exists, so + * "compiling" a profile yields a dummy handle and applying succeeds. + * Signatures match Apple's (the old stubs had wrong shapes). */ + +void* sandbox_compile_string(const char *profile, char **errorbuf) +{ + if (verbose) puts("STUB: sandbox_compile_string called"); + if (errorbuf != NULL) *errorbuf = NULL; + (void)profile; + return strdup("darling-sandbox-stub"); +} + +void* sandbox_compile_file(const char *path, char **errorbuf) { if (verbose) puts("STUB: sandbox_compile_file called"); - return NULL; + if (errorbuf != NULL) *errorbuf = NULL; + (void)path; + return strdup("darling-sandbox-stub"); } -void* sandbox_compile_named(void) +void* sandbox_compile_named(const char *name, char **errorbuf) { if (verbose) puts("STUB: sandbox_compile_named called"); - return NULL; + if (errorbuf != NULL) *errorbuf = NULL; + (void)name; + return strdup("darling-sandbox-stub"); } -void* sandbox_compile_string(void) +void* sandbox_compile_entitlements(void *entitlements, char **errorbuf) { - if (verbose) puts("STUB: sandbox_compile_string called"); - return NULL; + if (verbose) puts("STUB: sandbox_compile_entitlements called"); + if (errorbuf != NULL) *errorbuf = NULL; + (void)entitlements; + return strdup("darling-sandbox-stub"); } -void* sandbox_container_paths_iterate_items(void) +int sandbox_apply(void *handle) { - if (verbose) puts("STUB: sandbox_container_paths_iterate_items called"); - return NULL; + if (verbose) puts("STUB: sandbox_apply called"); + (void)handle; + return 0; } -void* sandbox_create_params(void) +void sandbox_free_profile(void *handle) { - if (verbose) puts("STUB: sandbox_create_params called"); + if (verbose) puts("STUB: sandbox_free_profile called"); + free(handle); +} + +void* sandbox_apply_container(void) +{ + if (verbose) puts("STUB: sandbox_apply_container called"); return NULL; } -void* sandbox_free_params(void) +void* sandbox_container_paths_iterate_items(void) { - if (verbose) puts("STUB: sandbox_free_params called"); + if (verbose) puts("STUB: sandbox_container_paths_iterate_items called"); return NULL; } -void* sandbox_free_profile(void) +void* sandbox_create_params(void) { - if (verbose) puts("STUB: sandbox_free_profile called"); + if (verbose) puts("STUB: sandbox_create_params called"); return NULL; } diff --git a/src/libsystem_coreservices/dirhelper.c b/src/libsystem_coreservices/dirhelper.c index f22f8213d..e7fbdc92e 100644 --- a/src/libsystem_coreservices/dirhelper.c +++ b/src/libsystem_coreservices/dirhelper.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -44,8 +45,18 @@ char* _dirhelper(int which, char* p, size_t limit) case DIRHELPER_USER_LOCAL: { const char* home; + // Darling: guest home is /Users/$USER even though the host + // HOME points at the Linux home; trustd needs the guest path + // for its cache locations. + const char *user = getenv("USER"); + char home_buf[PATH_MAX]; - home = getenv("HOME"); + if (user != NULL) { + snprintf(home_buf, sizeof(home_buf), "/Users/%s", user); + home = home_buf; + } else { + home = getenv("HOME"); + } if (home == NULL) home = ""; @@ -64,8 +75,15 @@ char* _dirhelper(int which, char* p, size_t limit) case DIRHELPER_USER_LOCAL_CACHE: { const char* home; + const char *user = getenv("USER"); + char home_buf[PATH_MAX]; - home = getenv("HOME"); + if (user != NULL) { + snprintf(home_buf, sizeof(home_buf), "/Users/%s", user); + home = home_buf; + } else { + home = getenv("HOME"); + } if (home != NULL) snprintf(p, limit, "%s/.cache/%s%s", home, dir_suffix ? dir_suffix : "", dir_suffix ? "/" : ""); else diff --git a/src/sandbox/sandbox.c b/src/sandbox/sandbox.c index f48cceb9e..9afa967db 100644 --- a/src/sandbox/sandbox.c +++ b/src/sandbox/sandbox.c @@ -10,7 +10,13 @@ const enum sandbox_filter_type SANDBOX_CHECK_NO_REPORT = SANDBOX_FILTER_NONE; int sandbox_init(const char *profile, uint64_t flags, char **errorbuf) { - *errorbuf = strdup("Not implemented"); + /* darling-lol: no seatbelt enforcement exists, so init trivially + * succeeds. Must tolerate errorbuf==NULL (Chromium passes NULL) and + * report no error on success; the old code crashed on NULL and left a + * bogus "Not implemented" error on success. */ + if (errorbuf != NULL) { + *errorbuf = NULL; + } return 0; } @@ -51,18 +57,36 @@ void sandbox_free_error(char *errorbuf) int sandbox_init_with_parameters(const char *profile, uint64_t flags, const char *const parameters[], char **errorbuf) { - *errorbuf = strdup("Not implemented"); + /* darling-lol: see sandbox_init. */ + if (errorbuf != NULL) { + *errorbuf = NULL; + } return 0; } int sandbox_init_with_extensions(const char *profile, uint64_t flags, const char *const extensions[], char **errorbuf) { - *errorbuf = strdup("Not implemented"); + /* darling-lol: see sandbox_init. Extensions are accepted and ignored. */ + if (errorbuf != NULL) { + *errorbuf = NULL; + } return 0; } int sandbox_check(pid_t pid, const char *operation, enum sandbox_filter_type type, ...) { + /* darling-lol: operation==NULL is the canonical "am I sandboxed?" + * probe (Chromium: sandbox_check(getpid(), NULL, SANDBOX_FILTER_NONE) + * "returns 1 if sandboxed", and ContentMainRunnerImpl CHECKs + * Seatbelt::IsSandboxed() in every sandboxed child -- a 0 here + * suicided all helpers/GPU with a silent UD2). Since init always + * "succeeds", consistently report sandboxed (1). Any other query + * keeps 0 (allowed), as nothing is enforced. */ + (void)pid; + if (operation == NULL) { + return 1; + } + (void)type; return 0; } @@ -84,42 +108,56 @@ int sandbox_unsuspend(void) int sandbox_issue_extension(const char *path, char **ext_token) { - return -1; + /* darling-lol: no enforcement; hand out a dummy token callers can + * consume/release. */ + if (ext_token != NULL) { + *ext_token = strdup("darling-sandbox-stub"); + } + return 0; } int sandbox_issue_fs_extension(const char *path, uint64_t flags, char **ext_token) { - return -1; + if (ext_token != NULL) { + *ext_token = strdup("darling-sandbox-stub"); + } + return 0; } int sandbox_issue_fs_rw_extension(const char *path, char **ext_token) { - return -1; + if (ext_token != NULL) { + *ext_token = strdup("darling-sandbox-stub"); + } + return 0; } int sandbox_issue_mach_extension(const char *name, char **ext_token) { - return -1; + if (ext_token != NULL) { + *ext_token = strdup("darling-sandbox-stub"); + } + return 0; } int sandbox_consume_extension(const char *path, const char *ext_token) { - return -1; + return 0; } int sandbox_consume_fs_extension(const char *ext_token, char **path) { - return -1; + return 0; } int sandbox_consume_mach_extension(const char *ext_token, char **name) { - return -1; + return 0; } int sandbox_release_fs_extension(const char *ext_token) { - return -1; + return 0; } int sandbox_container_path_for_pid(pid_t pid, char *buffer, size_t bufsize) @@ -129,7 +167,9 @@ int sandbox_container_path_for_pid(pid_t pid, char *buffer, size_t bufsize) int sandbox_wakeup_daemon(char **errorbuf) { - *errorbuf = strdup("Not implemented"); + if (errorbuf != NULL) { + *errorbuf = NULL; + } return -1; } diff --git a/src/sandbox/sandbox.h b/src/sandbox/sandbox.h index 6fa2d088c..655554284 100644 --- a/src/sandbox/sandbox.h +++ b/src/sandbox/sandbox.h @@ -161,6 +161,14 @@ int sandbox_note(const char *note); int sandbox_suspend(pid_t pid); int sandbox_unsuspend(void); +/* darling-lol: SeatbeltExtensions token family (used by Chromium). + * No enforcement exists, so issue dummy nonzero tokens and accept + * consume/release as no-ops. */ +int64_t sandbox_extension_issue_file(const char *path, uint32_t flags); +int64_t sandbox_extension_issue_mach(const char *name, uint32_t flags); +int sandbox_extension_consume(int64_t token); +int sandbox_extension_release(int64_t token); + int sandbox_issue_extension(const char *path, char **ext_token); int sandbox_issue_fs_extension(const char *path, uint64_t flags, char **ext_token); int sandbox_issue_fs_rw_extension(const char *path, char **ext_token); diff --git a/src/shellspawn/shellspawn.c b/src/shellspawn/shellspawn.c index f5ba615..53d04ad 100644 --- a/src/shellspawn/shellspawn.c +++ b/src/shellspawn/shellspawn.c @@ -34,9 +34,10 @@ along with Darling. If not, see . #include #include #include "shellspawn.h" +#include "shellspawn-fds.h" #include "duct_signals.h" -#define DBG 0 +#define DBG (getenv("DARLING_SHELLSPAWN_DEBUG") && strcmp(getenv("DARLING_SHELLSPAWN_DEBUG"), "1") == 0) int g_serverSocket = -1; struct sigaction sigchld_oldaction; @@ -50,6 +51,17 @@ void reapAll(void); int main(int argc, const char** argv) { + // darling-lol M58F40: ignore SIGPIPE. shellspawn writes to client/child + // sockets and pipes whose peers can close at any time (e.g. the `darling` + // client probing /var/run/shellspawn.sock and disconnecting, or a shell + // whose stdout pipe is closed). With the default disposition the first such + // write raises SIGPIPE and KILLS shellspawn, which removes the container's + // shell service and makes every subsequent `darling shell`/`darling ` + // fail with "Error connecting to shellspawn ... Connection refused" (the + // M58F39 "container wedge"). Every other daemon here (and darlingserver's + // M58f2 hardening) already ignores SIGPIPE; shellspawn was missed. + signal(SIGPIPE, SIG_IGN); + // shellspawn (daemon) --fork()--> shellspawn (child) --fork()--> exec /bin/bash // in order to read the exit status of the shell process, // we have to allow it to become a zombie, therefore we need to @@ -125,7 +137,7 @@ void spawnShell(int fd) { pid_t shell_pid = -1; int shellfd[3] = { -1, -1, -1 }; - int pipefd[2]; + int pipefd[2] = { -1, -1 }; int rv; struct pollfd pfd[2]; char** argv = NULL; @@ -143,6 +155,14 @@ void spawnShell(int fd) char* alloc_exec = NULL; + // Keep the control socket out of the stdio slots before receiving streams. + if (fd <= STDERR_FILENO) { + int moved = fcntl(fd, F_DUPFD_CLOEXEC, 3); + if (moved < 0) goto err; + close(fd); + fd = moved; + } + // Read commands from client while (read_cmds) { @@ -262,25 +282,20 @@ void spawnShell(int fd) argv = (char**) realloc(argv, sizeof(char*) * (argc + 1)); argv[argc] = NULL; - if (pipe(pipefd) == -1) - goto err; + if (shellspawn_remap_stdio(shellfd) < 0) goto err; + if (pipe(pipefd) == -1) goto err; setsid(); setpgrp(); - close(STDIN_FILENO); - close(STDOUT_FILENO); - close(STDERR_FILENO); - - dup2(shellfd[0], STDIN_FILENO); - dup2(shellfd[1], STDOUT_FILENO); - dup2(shellfd[2], STDERR_FILENO); - ioctl(STDIN_FILENO, TIOCSCTTY, STDIN_FILENO); shell_pid = fork(); + if (shell_pid < 0) goto err; if (shell_pid == 0) { + shellspawn_close_streams(shellfd); + close(pipefd[0]); close(fd); fcntl(pipefd[1], F_SETFD, FD_CLOEXEC); @@ -296,6 +311,7 @@ void spawnShell(int fd) exit(EXIT_FAILURE); } + shellspawn_close_streams(shellfd); if (alloc_exec) { free(alloc_exec); @@ -304,12 +320,14 @@ void spawnShell(int fd) // Check that exec succeeded close(pipefd[1]); // close the write end + pipefd[1] = -1; if (read(pipefd[0], &rv, sizeof(rv)) == sizeof(rv)) { errno = rv; goto err; } close(pipefd[0]); + pipefd[0] = -1; // Now we start passing signals // and check for child process exit @@ -372,7 +390,7 @@ void spawnShell(int fd) if (darwin_signal != 0) { - int fg_pid = tcgetpgrp(shellfd[0]); + int fg_pid = tcgetpgrp(STDIN_FILENO); if (fg_pid != -1) { if (DBG) printf("fg_pid = %d\n", fg_pid); @@ -394,11 +412,7 @@ void spawnShell(int fd) kill(shell_pid, SIGKILL); // Close shell fds - for (int i = 0; i < 3; i++) - { - if (shellfd[i] != -1) - close(shellfd[0]); - } + shellspawn_close_streams(shellfd); // Reap the child int wstatus; @@ -415,13 +429,11 @@ void spawnShell(int fd) reapAll(); return; err: + if (pipefd[0] >= 0) close(pipefd[0]); + if (pipefd[1] >= 0) close(pipefd[1]); if (DBG) fprintf(stderr, "Error spawning shell: %s\n", strerror(errno)); - for (int i = 0; i < 3; i++) - { - if (shellfd[i] != -1) - close(shellfd[0]); - } + shellspawn_close_streams(shellfd); if (shell_pid != -1) kill(shell_pid, SIGKILL); diff --git a/src/startup/mldr/commpage.c b/src/startup/mldr/commpage.c index d9a7aea96..cc76f7a85 100644 --- a/src/startup/mldr/commpage.c +++ b/src/startup/mldr/commpage.c @@ -12,6 +12,7 @@ // Include commpage definitions #define PRIVATE #include +#include "commpage-cpu-caps.h" static const char* SIGNATURE32 = "commpage 32-bit"; static const char* SIGNATURE64 = "commpage 64-bit"; @@ -84,74 +85,26 @@ void commpage_setup(bool _64bit) uint64_t get_cpu_caps(void) { - uint64_t caps = 0; - - { - union cpu_flags1 eax; - union cpu_flags2 ecx; - union cpu_flags3 edx; - uint32_t ebx; - - eax.reg = ecx.reg = edx.reg = 0; - __cpuid(1, eax.reg, ebx, ecx.reg, edx.reg); - - if (ecx.mmx) - caps |= kHasMMX; - if (ecx.sse) - caps |= kHasSSE; - if (ecx.sse2) - caps |= kHasSSE2; - if (edx.sse3) - caps |= kHasSSE3; - if (ecx.ia64) - caps |= k64Bit; - if (edx.ssse3) - caps |= kHasSupplementalSSE3; - if (edx.sse41) - caps |= kHasSSE4_1; - if (edx.sse42) - caps |= kHasSSE4_2; - if (edx.aes) - caps |= kHasAES; - if (edx.avx) - caps |= kHasAVX1_0; - if (edx.rdrnd) - caps |= kHasRDRAND; - if (edx.fma) - caps |= kHasFMA; - if (edx.f16c) - caps |= kHasF16C; - } - { - union cpu_flags4 ebx; - union cpu_flags5 ecx; - uint32_t edx = 0, eax = 7; - - __cpuid(7, eax, ebx.reg, ecx.reg, edx); - - if (ebx.erms) - caps |= kHasENFSTRG; - if (ebx.avx2) - caps |= kHasAVX2_0; - if (ebx.bmi1) - caps |= kHasBMI1; - if (ebx.bmi2) - caps |= kHasBMI2; - if (ebx.rtm) - caps |= kHasRTM; - if (ebx.hle) - caps |= kHasHLE; - if (ebx.rdseed) - caps |= kHasRDSEED; - if (ebx.adx) - caps |= kHasADX; - if (ebx.mpx) - caps |= kHasMPX; - if (ebx.sgx) - caps |= kHasSGX; - } - - return caps; + unsigned int a, b, ecx1 = 0, edx1 = 0, ebx7 = 0; + const unsigned int max_basic = __get_cpuid_max(0, NULL); + if (max_basic >= 1) __cpuid_count(1, 0, a, b, ecx1, edx1); + if (max_basic >= 7) { + unsigned int c, d; + __cpuid_count(7, 0, a, ebx7, c, d); + } + uint64_t xcr0 = 0; + if ((ecx1 & ((1u << 26) | (1u << 27))) == ((1u << 26) | (1u << 27))) { + unsigned int low, high; + __asm__ volatile("xgetbv" : "=a"(low), "=d"(high) : "c"(0)); + xcr0 = ((uint64_t)high << 32) | low; + } + int long_mode = 0; + if (__get_cpuid_max(0x80000000u, NULL) >= 0x80000001u) { + unsigned int c, d; + __cpuid_count(0x80000001u, 0, a, b, c, d); + long_mode = !!(d & (1u << 29)); + } + return commpage_decode_cpu_caps(ecx1, edx1, ebx7, xcr0, long_mode); } unsigned long commpage_address(bool _64bit) diff --git a/src/startup/mldr/elfcalls/elfcalls.c b/src/startup/mldr/elfcalls/elfcalls.c index 8f0b9e61f..38a9644c0 100644 --- a/src/startup/mldr/elfcalls/elfcalls.c +++ b/src/startup/mldr/elfcalls/elfcalls.c @@ -6,6 +6,7 @@ #include #include #include +#include #include "elfcalls.h" #include "threads.h" #include @@ -86,6 +87,9 @@ static int __dserver_process_lifetime_pipe_refresh() { void elfcalls_make(struct elf_calls* calls) { + extern void __mldr_fork_prepare(void); + extern void __mldr_fork_complete(void); + pthread_atfork(__mldr_fork_prepare, __mldr_fork_complete, __mldr_fork_complete); calls->dlopen = dlopen_simple; calls->dlclose = dlclose; calls->dlsym = dlsym; @@ -100,7 +104,16 @@ void elfcalls_make(struct elf_calls* calls) calls->darling_thread_get_stack = __darling_thread_get_stack; calls->get_errno = get_errno; - calls->exit = exit; + // darling-lol: the guest's `exit()` has already run its own + // `__cxa_finalize`/atexit handlers by the time the exit syscall + // reaches `native_exit`. Using the host `exit()` here ALSO runs the + // host libc's atexit handlers, which include host Mesa/lavapipe + // (`libgallium`) thread-pool teardown. In a forked child the worker + // threads don't exist, so that teardown blocks forever in + // `pthread_cond_wait` and the parent's `wait4` never returns + // (crashpad::SpawnSubprocess hang). The exit syscall must not run + // atexit handlers; use `_exit`. + calls->exit = _exit; calls->malloc = malloc; calls->free = free; @@ -126,4 +139,5 @@ void elfcalls_make(struct elf_calls* calls) calls->dserver_get_process_lifetime_pipe = __dserver_get_process_lifetime_pipe; calls->dserver_process_lifetime_pipe_refresh = __dserver_process_lifetime_pipe_refresh; calls->dserver_close_process_lifetime_pipe = __mldr_close_process_lifetime_pipe; + calls->fork = fork; } diff --git a/src/startup/mldr/elfcalls/elfcalls.h b/src/startup/mldr/elfcalls/elfcalls.h index fee1bde87..bee86604b 100644 --- a/src/startup/mldr/elfcalls/elfcalls.h +++ b/src/startup/mldr/elfcalls/elfcalls.h @@ -71,7 +71,9 @@ struct elf_calls int (*dserver_get_process_lifetime_pipe)(void); int (*dserver_process_lifetime_pipe_refresh)(void); void (*dserver_close_process_lifetime_pipe)(int fd); + + // Host libc fork prepares native allocator and loader locks for the child. + int (*fork)(void); }; #endif - diff --git a/src/startup/mldr/mldr.c b/src/startup/mldr/mldr.c index 6617ef82b..a227abee8 100644 --- a/src/startup/mldr/mldr.c +++ b/src/startup/mldr/mldr.c @@ -154,7 +154,7 @@ int main(int argc, char** argv, char** envp) // allow any process to ptrace us // the only process we really care about being able to do this is the server, // but we can't just use the server's PID, since it lies outside our PID namespace. - ptrace(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0); + prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0); process_special_env(&mldr_load_results); @@ -563,6 +563,16 @@ static socket_bitmap_t socket_bitmap = { .highest = -1, }; +/* Host fork must preserve a consistent bitmap and an unlocked child mutex. + * These callbacks run through glibc fork, before guest RPC fork cleanup. */ +void __mldr_fork_prepare(void) { + pthread_mutex_lock(&socket_bitmap.mutex); +} + +void __mldr_fork_complete(void) { + pthread_mutex_unlock(&socket_bitmap.mutex); +} + static int socket_bitmap_get(socket_bitmap_t* bitmap) { int fd = -1; bool updated = false; @@ -648,6 +658,11 @@ static void socket_bitmap_put(socket_bitmap_t* bitmap, int socket) { pthread_mutex_lock(&bitmap->mutex); + // darling-lol: placed above the slots because its slot was taken (socket_bitmap_place). + if (socket > bitmap->highest) { + goto out; + } + index = bitmap->highest - socket; bitmap->bits[index / 8] &= ~(1 << (index % 8)); @@ -673,7 +688,13 @@ static void socket_bitmap_put(socket_bitmap_t* bitmap, int socket) { } } - if ((bitmap->bits[index / 8] & (1 << (index % 8))) == 0) { + // darling-lol: the test was inverted (`== 0`, i.e. it stopped at a FREE slot and shrank past slots that were + // IN USE). After a whole trailing byte of slots had been released, the length collapsed below live + // sockets; the next threads were then handed descriptor numbers that still belonged to running threads, + // dup2() replaced those threads' RPC sockets, and when the newcomer exited the older thread was left with a + // closed descriptor (EBADF on its next call, process abort). Seen as LeagueClient dying ~85-110 s after start: + // slot 71 (fd 524216), held by a long-sleeping thread, given to a second thread (logs/rpc-fd-history.log). + if ((bitmap->bits[index / 8] & (1 << (index % 8))) != 0) { // this bit is in-use, so we can't shrink any further break; } @@ -685,12 +706,20 @@ static void socket_bitmap_put(socket_bitmap_t* bitmap, int socket) { if (old_byte_size != new_byte_size) { // we can free one or more bytes from the bitmap - void* ptr = realloc(bitmap->bits, new_byte_size); - if (!ptr) { - goto out; - } + if (new_byte_size == 0) { + // darling-lol: realloc(ptr, 0) frees the block and returns NULL; the old code then kept the dangling + // pointer and the next allocation aborted with a double free. (Unreachable while the shrink test + // above was inverted; reached as soon as a process released its last socket.) + free(bitmap->bits); + bitmap->bits = NULL; + } else { + void* ptr = realloc(bitmap->bits, new_byte_size); + if (!ptr) { + goto out; + } - bitmap->bits = ptr; + bitmap->bits = ptr; + } } } @@ -698,6 +727,22 @@ out: pthread_mutex_unlock(&bitmap->mutex); }; +/* darling-lol: move `fd` to the next slot. dup2() silently closed whatever the guest or a native library (NVIDIA's + * driver: one fd per allocation) already held on that number, and its owner later closed our socket. F_DUPFD never + * replaces a descriptor: if the slot is taken, it returns the lowest free number above it, which is ours alone. */ +static int socket_bitmap_place(int fd) { + int slot = socket_bitmap_get(&socket_bitmap); + if (slot < 0) { + return -1; + } + + int placed = fcntl(fd, F_DUPFD, slot); + if (placed != slot) { + socket_bitmap_put(&socket_bitmap, slot); + } + return placed; +} + int __mldr_create_rpc_socket(void) { int pre_fd = -1; int fd = -1; @@ -707,18 +752,11 @@ int __mldr_create_rpc_socket(void) { goto err_out; } - fd = socket_bitmap_get(&socket_bitmap); + fd = socket_bitmap_place(pre_fd); if (fd < 0) { goto err_out; } - if (dup2(pre_fd, fd) < 0) { - // we have to put it away ourselves here because `fd` is not yet valid, so we can't close() it in the error handler - socket_bitmap_put(&socket_bitmap, fd); - fd = -1; - goto err_out; - } - close(pre_fd); pre_fd = -1; @@ -772,17 +810,11 @@ int __mldr_create_process_lifetime_pipe(int* fds) { } for (int i = 0; i < 2; ++i) { - fds[i] = socket_bitmap_get(&socket_bitmap); + fds[i] = socket_bitmap_place(pre_fds[i]); if (fds[i] < 0) { goto err_out; } - if (dup2(pre_fds[i], fds[i]) < 0) { - socket_bitmap_put(&socket_bitmap, fds[i]); - fds[i] = -1; - goto err_out; - } - close(pre_fds[i]); pre_fds[i] = -1; } diff --git a/src/tools/open.m b/src/tools/open.m index 126bd394c..f62c2e0e8 100644 --- a/src/tools/open.m +++ b/src/tools/open.m @@ -150,7 +150,15 @@ int main(int argc, char *argv[]) NSURL** urls = (NSURL**) malloc(count * sizeof(NSURL*)); for (int i = 0; i < count; i++) - urls[i] = [NSURL URLWithString:[NSString stringWithCString: argv[optind+i] encoding: NSUTF8StringEncoding]]; + { + NSString* str = [NSString stringWithCString: argv[optind+i] encoding: NSUTF8StringEncoding]; + // +URLWithString: needs a scheme (e.g. file:/// or https://) and + // returns nil for bare POSIX paths, which is how `open` is most + // often invoked. Fall back to a file URL in that case. + urls[i] = [NSURL URLWithString: str]; + if (urls[i] == nil) + urls[i] = [NSURL fileURLWithPath: str]; + } filenames = [NSArray arrayWithObjects: urls count: count]; free(urls); diff --git a/src/CoreAudio/AudioTrace.h b/src/CoreAudio/AudioTrace.h new file mode 100644 --- /dev/null +++ b/src/CoreAudio/AudioTrace.h @@ -0,0 +1,37 @@ +#ifndef RCLOL_AUDIO_TRACE_H +#define RCLOL_AUDIO_TRACE_H +#include +#include +#include +#include +#include +#include +// Opt-in API/status diagnostics only; never records audio samples or names. +// Enabled by env RCLOL_AUDIO_TRACE or by the existence of /tmp/rclol-audio-trace +// or /Users/Shared/rclol-audio-trace (the latter survives /tmp wipes at launch). +// A negative result is re-checked at most once every ~2 seconds; a positive +// result is cached for the life of the process. +static inline bool rclolAudioTraceEnabled() { + static std::atomic enabled{false}; + static std::atomic nextCheckMs{0}; + if (enabled.load(std::memory_order_relaxed)) return true; + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + long long nowMs = (long long) ts.tv_sec * 1000 + ts.tv_nsec / 1000000 + 1; + if (nowMs < nextCheckMs.load(std::memory_order_relaxed)) return false; + nextCheckMs.store(nowMs + 2000, std::memory_order_relaxed); + if (getenv("RCLOL_AUDIO_TRACE") != nullptr || + access("/tmp/rclol-audio-trace", F_OK) == 0 || + access("/Users/Shared/rclol-audio-trace", F_OK) == 0) { + enabled.store(true, std::memory_order_relaxed); + return true; + } + return false; +} +static inline void rclolAudioTrace(const char* format, ...) { + if (!rclolAudioTraceEnabled()) return; + static FILE* file=[] { FILE* f=fopen("/tmp/rclol-audio-api.log","a"); if(f) setvbuf(f,nullptr,_IOLBF,0); return f; }(); + if(!file) return; + va_list args;va_start(args,format);fprintf(file,"pid=%d ",getpid());vfprintf(file,format,args);va_end(args); +} +#endif diff --git a/src/CoreAudio/CoreAudioComponent/MatrixMixerAU.cpp b/src/CoreAudio/CoreAudioComponent/MatrixMixerAU.cpp new file mode 100644 --- /dev/null +++ b/src/CoreAudio/CoreAudioComponent/MatrixMixerAU.cpp @@ -0,0 +1,175 @@ +#include "../AudioTrace.h" +/* This file is part of Darling, distributed under the GNU GPL v3 or later. */ +#include "AUBase.h" +#include "AUInputElement.h" +#include "AUOutputElement.h" +#include "AUPlugInDispatch.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// Float PCM matrix mixing. Channel indices are flattened across each scope's buses. +class MatrixMixerAU : public AUBase { + std::mutex mMutex; + std::map mGains; + std::map mInputEnabled, mOutputEnabled; + UInt32 channels(AudioUnitScope scope) { + auto& buses = GetScope(scope); + UInt32 count = 0; + for (UInt32 b = 0; b < buses.GetNumberOfElements(); ++b) + count += buses.GetIOElement(b)->GetStreamFormat().mChannelsPerFrame; + return count; + } + static Float32 value(const std::map& values, UInt32 key, Float32 fallback = 0) { + auto it = values.find(key); + return it == values.end() ? fallback : it->second; + } + bool validGain(UInt32 element) { + UInt32 input = element >> 16, output = element & 0xffff; + return (input == 0xffff || input < channels(kAudioUnitScope_Input)) && + (output == 0xffff || output < channels(kAudioUnitScope_Output)); + } + static Float32* sample(AudioBufferList& buffers, UInt32 channel, UInt32& stride) { + for (UInt32 b = 0; b < buffers.mNumberBuffers; ++b) { + AudioBuffer& buffer = buffers.mBuffers[b]; + if (channel < buffer.mNumberChannels) { + stride = buffer.mNumberChannels; + return buffer.mData ? static_cast(buffer.mData) + channel : nullptr; + } + channel -= buffer.mNumberChannels; + } + return nullptr; + } +public: + MatrixMixerAU(AudioComponentInstance instance) : AUBase(instance, 1, 1) {} + bool CanScheduleParameters() const override { return false; } + bool StreamFormatWritable(AudioUnitScope, AudioUnitElement) override { return !IsInitialized(); } + bool BusCountWritable(AudioUnitScope scope) override { + return scope == kAudioUnitScope_Input || scope == kAudioUnitScope_Output; + } + bool ValidFormat(AudioUnitScope scope, AudioUnitElement, const CAStreamBasicDescription& f) override { + if (!f.IsPCM() || !f.IsFloat() || f.mBitsPerChannel != 32 || + !f.mChannelsPerFrame || f.mChannelsPerFrame >= 0xffff || f.mSampleRate <= 0 || + (f.mFormatFlags & kAudioFormatFlagIsBigEndian) || f.mFramesPerPacket != 1) + return false; + UInt32 bytes = 4 * (f.IsInterleaved() ? f.mChannelsPerFrame : 1); + return f.mBytesPerFrame == bytes && f.mBytesPerPacket == bytes; + } + OSStatus Initialize() override { + if (!Inputs().GetNumberOfElements() || !Outputs().GetNumberOfElements() || + channels(kAudioUnitScope_Input) >= 0xffff || channels(kAudioUnitScope_Output) >= 0xffff) + return kAudioUnitErr_FormatNotSupported; + /* darling-lol: the rate must agree across the buses that are in use. League declares more buses than it uses (it + * sets the element counts and configures bus 0 only); the unused ones keep a default rate and used to make this + * initialize fail with FormatNotSupported, which sent the game into an endless graph rebuild loop. Input buses + * that have no source are never pulled (RenderBus skips them), and only output bus 0 is ever connected. */ + double rate = GetOutput(0)->GetStreamFormat().mSampleRate; + for (UInt32 b = 0; b < Inputs().GetNumberOfElements(); ++b) { + const double r = GetInput(b)->GetStreamFormat().mSampleRate; + if (GetInput(b)->IsActive() && r != rate) { + rclolAudioTrace("MIXER_INIT_RATE_MISMATCH input=%u rate=%.0f output0=%.0f\n", b, r, rate); + return kAudioUnitErr_FormatNotSupported; + } + } + return AUBase::Initialize(); + } + OSStatus SetParameter(AudioUnitParameterID id, AudioUnitScope scope, AudioUnitElement element, + AudioUnitParameterValue gain, UInt32) override { + static std::atomic traceCount{0}; + if (rclolAudioTraceEnabled() && traceCount++ < 64) + rclolAudioTrace("AUDIO_MATRIX_PARAM id=%u scope=%u element=%08x gain=%g\n",id,scope,element,gain); + std::lock_guard lock(mMutex); + if (!std::isfinite(gain)) return kAudioUnitErr_InvalidParameter; + if (id == kMatrixMixerParam_Volume) { + if (scope == kAudioUnitScope_Input) { + if (element >= channels(scope)) return kAudioUnitErr_InvalidElement; + element = (element << 16) | 0xffff; + } else if (scope == kAudioUnitScope_Output) { + if (element >= channels(scope)) return kAudioUnitErr_InvalidElement; + element |= 0xffff0000; + } else if (scope != kAudioUnitScope_Global) return kAudioUnitErr_InvalidScope; + if (!validGain(element)) return kAudioUnitErr_InvalidElement; + mGains[element] = gain; + return noErr; + } + if (id == kMatrixMixerParam_Enable && + (scope == kAudioUnitScope_Input || scope == kAudioUnitScope_Output)) { + if (element >= GetScope(scope).GetNumberOfElements()) return kAudioUnitErr_InvalidElement; + (scope == kAudioUnitScope_Input ? mInputEnabled : mOutputEnabled)[element] = gain != 0; + return noErr; + } + return kAudioUnitErr_InvalidParameter; + } + OSStatus GetParameter(AudioUnitParameterID id, AudioUnitScope scope, AudioUnitElement element, + AudioUnitParameterValue& gain) override { + std::lock_guard lock(mMutex); + if (id == kMatrixMixerParam_Volume) { + if (scope == kAudioUnitScope_Input || scope == kAudioUnitScope_Output) { + if (element >= channels(scope)) return kAudioUnitErr_InvalidElement; + element = scope == kAudioUnitScope_Input ? (element << 16) | 0xffff : element | 0xffff0000; + } else if (scope != kAudioUnitScope_Global) return kAudioUnitErr_InvalidScope; + if (!validGain(element)) return kAudioUnitErr_InvalidElement; + gain = value(mGains, element); + return noErr; + } + if (id == kMatrixMixerParam_Enable && + (scope == kAudioUnitScope_Input || scope == kAudioUnitScope_Output)) { + if (element >= GetScope(scope).GetNumberOfElements()) return kAudioUnitErr_InvalidElement; + gain = value(scope == kAudioUnitScope_Input ? mInputEnabled : mOutputEnabled, element, 1); + return noErr; + } + return kAudioUnitErr_InvalidParameter; + } + OSStatus RenderBus(AudioUnitRenderActionFlags& flags, const AudioTimeStamp& time, + UInt32 bus, UInt32 frames) override { + auto& output = GetOutput(bus)->GetBufferList(); + for (UInt32 b = 0; b < output.mNumberBuffers; ++b) + if (output.mBuffers[b].mData) memset(output.mBuffers[b].mData, 0, output.mBuffers[b].mDataByteSize); + UInt32 outputOffset = 0; + for (UInt32 b = 0; b < bus; ++b) outputOffset += GetOutput(b)->GetStreamFormat().mChannelsPerFrame; + UInt32 inputOffset = 0; + bool silent = true; + for (UInt32 b = 0; b < Inputs().GetNumberOfElements(); ++b) { + auto* input = GetInput(b); + UInt32 inputChannels = input->GetStreamFormat().mChannelsPerFrame; + // Pull without holding the parameter lock: callbacks may change parameters. + if (input->IsActive()) { + AudioUnitRenderActionFlags inputFlags = 0; + OSStatus status = input->PullInput(inputFlags, time, b, frames); + if (status) return status; + auto& data = input->GetBufferList(); + std::lock_guard lock(mMutex); + if (value(mInputEnabled, b, 1) && value(mOutputEnabled, bus, 1) && + !(inputFlags & kAudioUnitRenderAction_OutputIsSilence)) { + for (UInt32 i = 0; i < inputChannels; ++i) { + UInt32 inputStride = 0; + Float32* src = sample(data, i, inputStride); + for (UInt32 o = 0; o < GetOutput(bus)->GetStreamFormat().mChannelsPerFrame; ++o) { + UInt32 outputStride = 0; + Float32* dst = sample(output, o, outputStride); + Float32 gain = value(mGains, 0xffffffff) * + value(mGains, ((inputOffset+i) << 16) | 0xffff) * + value(mGains, 0xffff0000 | (outputOffset+o)) * + value(mGains, ((inputOffset+i) << 16) | (outputOffset+o)); + if (!src || !dst || !gain) continue; + for (UInt32 f = 0; f < frames; ++f) dst[f*outputStride] += src[f*inputStride] * gain; + silent = false; + } + } + } + } + inputOffset += inputChannels; + } + if (silent) flags |= kAudioUnitRenderAction_OutputIsSilence; + else flags &= ~kAudioUnitRenderAction_OutputIsSilence; + return noErr; + } +}; +#pragma GCC visibility push(default) +AUDIOCOMPONENT_ENTRY(AUBaseFactory, MatrixMixerAU); +#pragma GCC visibility pop diff --git a/src/frameworks/Carbon/HIToolbox/src/SecureInput.c b/src/frameworks/Carbon/HIToolbox/src/SecureInput.c new file mode 100644 --- /dev/null +++ b/src/frameworks/Carbon/HIToolbox/src/SecureInput.c @@ -0,0 +1,14 @@ +/* X11 does not provide the macOS secure-event or TSM document facilities. + * Export the entry points and report unsupported instead of aborting in dyld. + * Do not claim that ordinary X11 keyboard input has become secure. */ +int EnableSecureEventInput(void) { return -4; } +int DisableSecureEventInput(void) { return -4; } +int TSMSetDocumentProperty(void *document, unsigned tag, unsigned size, + void *data) { + (void)document; (void)tag; (void)size; (void)data; + return -4; +} +int TSMRemoveDocumentProperty(void *document, unsigned tag) { + (void)document; (void)tag; + return -4; +} diff --git a/src/frameworks/CoreMedia/include/CoreMedia/CMFormatDescription.h b/src/frameworks/CoreMedia/include/CoreMedia/CMFormatDescription.h new file mode 100644 --- /dev/null +++ b/src/frameworks/CoreMedia/include/CoreMedia/CMFormatDescription.h @@ -0,0 +1,40 @@ +#ifndef _COREMEDIA_CMFORMATDESCRIPTION_H_ +#define _COREMEDIA_CMFORMATDESCRIPTION_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern const CFStringRef kCMFormatDescriptionTransferFunction_ITU_R_709_2; +extern const CFStringRef kCMFormatDescriptionTransferFunction_SMPTE_240M_1995; +extern const CFStringRef kCMFormatDescriptionTransferFunction_UseGamma; +extern const CFStringRef kCMFormatDescriptionTransferFunction_ITU_R_2020; +extern const CFStringRef kCMFormatDescriptionTransferFunction_ITU_R_2100_HLG; +extern const CFStringRef kCMFormatDescriptionTransferFunction_SMPTE_ST_2084_PQ; +extern const CFStringRef kCMFormatDescriptionTransferFunction_SMPTE_ST_428_1; +extern const CFStringRef kCMFormatDescriptionTransferFunction_Linear; +extern const CFStringRef kCMFormatDescriptionTransferFunction_sRGB; +extern const CFStringRef kCMFormatDescriptionColorPrimaries_ITU_R_709_2; +extern const CFStringRef kCMFormatDescriptionColorPrimaries_EBU_3213; +extern const CFStringRef kCMFormatDescriptionColorPrimaries_SMPTE_C; +extern const CFStringRef kCMFormatDescriptionColorPrimaries_ITU_R_2020; +extern const CFStringRef kCMFormatDescriptionColorPrimaries_P3_D65; +extern const CFStringRef kCMFormatDescriptionColorPrimaries_DCI_P3; +extern const CFStringRef kCMFormatDescriptionExtension_ContentLightLevelInfo; +extern const CFStringRef kCMFormatDescriptionExtension_Depth; +extern const CFStringRef kCMFormatDescriptionExtension_FormatName; +extern const CFStringRef kCMFormatDescriptionExtension_FullRangeVideo; +extern const CFStringRef kCMFormatDescriptionExtension_MasteringDisplayColorVolume; +extern const CFStringRef kCMSampleAttachmentKey_IsDependedOnByOthers; +extern const CFStringRef kCMFormatDescriptionYCbCrMatrix_ITU_R_709_2; +extern const CFStringRef kCMFormatDescriptionYCbCrMatrix_ITU_R_601_4; +extern const CFStringRef kCMFormatDescriptionYCbCrMatrix_SMPTE_240M_1995; +extern const CFStringRef kCMFormatDescriptionYCbCrMatrix_ITU_R_2020; + +#ifdef __cplusplus +} +#endif + +#endif // _COREMEDIA_CMFORMATDESCRIPTION_H_ diff --git a/src/frameworks/CryptoTokenKit/src/TKTokenWatcher.m b/src/frameworks/CryptoTokenKit/src/TKTokenWatcher.m new file mode 100644 --- /dev/null +++ b/src/frameworks/CryptoTokenKit/src/TKTokenWatcher.m @@ -0,0 +1,60 @@ +#import +#import + +@protocol TKProtocolTokenWatcher +@end + +@implementation TKTokenWatcher + +- (id)init +{ + if ((self = [super init]) != nil) { + _tokenIDs = [[NSMutableArray alloc] init]; + _removalHandlers = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (id)initWithInsertionHandler:(id)arg1 +{ + if ((self = [self init]) != nil) { + _insertionHandler = [arg1 copy]; + } + return self; +} + +- (void)dealloc +{ + [_insertionHandler release]; + [_removalHandlers release]; + [_tokenIDs release]; + [super dealloc]; +} + +- (NSArray *)tokenIDs +{ + return [NSArray array]; +} + +- (void)addRemovalHandler:(id)arg1 forTokenID:(id)arg2 +{ + if (arg1 && arg2) + [_removalHandlers setObject: [[arg1 copy] autorelease] forKey: arg2]; +} + +- (void)insertedToken:(id)arg1 reply:(id)arg2 +{ + // No smartcard/token hardware under Darling; ignore. +} + +- (void)removedToken:(id)arg1 reply:(id)arg2 +{ + // No smartcard/token hardware under Darling; ignore. +} + +- (void)setup +{ + // No token daemon to connect to; remain empty. +} + +@end diff --git a/src/frameworks/OpenDirectory/src/ODLocalDirectory.h b/src/frameworks/OpenDirectory/src/ODLocalDirectory.h new file mode 100644 --- /dev/null +++ b/src/frameworks/OpenDirectory/src/ODLocalDirectory.h @@ -0,0 +1,37 @@ +/* + This file is part of Darling. + + Copyright (C) 2026 Darling Team + + Darling is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Darling is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Darling. If not, see . +*/ + +#import +#import + +// The local node: user records read from the system account database. +// Every value is an array of strings, keyed by attribute type. + +extern NSString *const ODLocalNodeName; + +NSError *ODLocalError(NSInteger code); +// nil when the name is not one of the nodes a standalone system has +NSString *ODLocalNodeNameForName(NSString *name); +NSString *ODLocalNodeNameForType(unsigned int type); +NSDictionary *ODLocalUserAttributes(NSString *name); +NSArray *ODLocalAllUserAttributes(void); +// a single record type or a list of them +BOOL ODLocalRecordTypesIncludeUsers(id recordTypeOrList); + +ODRecord *ODLocalRecordCreate(NSDictionary *attributes); diff --git a/src/frameworks/OpenDirectory/src/ODLocalDirectory.m b/src/frameworks/OpenDirectory/src/ODLocalDirectory.m new file mode 100644 --- /dev/null +++ b/src/frameworks/OpenDirectory/src/ODLocalDirectory.m @@ -0,0 +1,141 @@ +/* + This file is part of Darling. + + Copyright (C) 2026 Darling Team + + Darling is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Darling is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Darling. If not, see . +*/ + +#import "ODLocalDirectory.h" + +#include +#include +#include +#include +#include + +NSString *const ODFrameworkErrorDomain = @"com.apple.OpenDirectory"; +NSString *const ODLocalNodeName = @"/Local/Default"; + +// getpwent() iterates process-wide state +static pthread_mutex_t enumerationLock = PTHREAD_MUTEX_INITIALIZER; + +NSError *ODLocalError(NSInteger code) +{ + return [NSError errorWithDomain: ODFrameworkErrorDomain code: code userInfo: nil]; +} + +NSString *ODLocalNodeNameForName(NSString *name) +{ + for (NSString *known in @[ODLocalNodeName, @"/Search", @"/Contacts"]) { + if ([known isEqualToString: name]) + return known; + } + return nil; +} + +NSString *ODLocalNodeNameForType(unsigned int type) +{ + switch (type) { + case 0x2201: // kODNodeTypeAuthentication + case 0x2205: // kODNodeTypeNetwork + return @"/Search"; + case 0x2204: // kODNodeTypeContacts + return @"/Contacts"; + default: + return ODLocalNodeName; + } +} + +static NSDictionary *attributesForEntry(const struct passwd *entry) +{ + NSMutableDictionary *attributes = [NSMutableDictionary dictionary]; + NSString *name = [NSString stringWithUTF8String: entry->pw_name]; + uuid_t uuid; + + if (name == nil) + return nil; + + attributes[@"dsAttrTypeStandard:RecordName"] = @[name]; + attributes[@"dsAttrTypeStandard:RecordType"] = @[@"dsRecTypeStandard:Users"]; + attributes[@"dsAttrTypeStandard:AppleMetaNodeLocation"] = @[ODLocalNodeName]; + attributes[@"dsAttrTypeStandard:UniqueID"] = @[[NSString stringWithFormat: @"%u", (unsigned int)entry->pw_uid]]; + attributes[@"dsAttrTypeStandard:PrimaryGroupID"] = @[[NSString stringWithFormat: @"%u", (unsigned int)entry->pw_gid]]; + + if (mbr_uid_to_uuid(entry->pw_uid, uuid) != 0) { + // No stored UUID: use the membership library's compatibility mapping + // for such accounts, FFFFEEEE-DDDD-CCCC-BBBB-AAAA followed by the uid. + static const uuid_t prefix = {0xff, 0xff, 0xee, 0xee, 0xdd, 0xdd, 0xcc, 0xcc, 0xbb, 0xbb, 0xaa, 0xaa}; + uint32_t uid = htonl(entry->pw_uid); + + memcpy(uuid, prefix, sizeof(uuid_t)); + memcpy(&uuid[12], &uid, sizeof(uid)); + } + uuid_string_t text; + uuid_unparse_upper(uuid, text); + attributes[@"dsAttrTypeStandard:GeneratedUID"] = @[[NSString stringWithUTF8String: text]]; + + NSString *value; + if (entry->pw_gecos != NULL && (value = [NSString stringWithUTF8String: entry->pw_gecos]) != nil) + attributes[@"dsAttrTypeStandard:RealName"] = @[value]; + if (entry->pw_dir != NULL && (value = [NSString stringWithUTF8String: entry->pw_dir]) != nil) + attributes[@"dsAttrTypeStandard:NFSHomeDirectory"] = @[value]; + if (entry->pw_shell != NULL && (value = [NSString stringWithUTF8String: entry->pw_shell]) != nil) + attributes[@"dsAttrTypeStandard:UserShell"] = @[value]; + + return attributes; +} + +NSDictionary *ODLocalUserAttributes(NSString *name) +{ + struct passwd entry, *result = NULL; + char buffer[4096]; + + if (![name isKindOfClass: [NSString class]] || [name UTF8String] == NULL) + return nil; + if (getpwnam_r([name UTF8String], &entry, buffer, sizeof(buffer), &result) != 0 || result == NULL) + return nil; + + return attributesForEntry(result); +} + +NSArray *ODLocalAllUserAttributes(void) +{ + NSMutableArray *users = [NSMutableArray array]; + struct passwd *entry; + + pthread_mutex_lock(&enumerationLock); + setpwent(); + while ((entry = getpwent()) != NULL) { + NSDictionary *attributes = attributesForEntry(entry); + if (attributes != nil) + [users addObject: attributes]; + } + endpwent(); + pthread_mutex_unlock(&enumerationLock); + + return users; +} + +BOOL ODLocalRecordTypesIncludeUsers(id recordTypeOrList) +{ + NSArray *types = [recordTypeOrList isKindOfClass: [NSArray class]] ? recordTypeOrList + : recordTypeOrList != nil ? @[recordTypeOrList] : @[]; + + for (id type in types) { + if ([type isKindOfClass: [NSString class]] && [type isEqualToString: @"dsRecTypeStandard:Users"]) + return YES; + } + return NO; +} diff --git a/src/frameworks/OpenGL/CGLAsyncPresent.inc b/src/frameworks/OpenGL/CGLAsyncPresent.inc new file mode 100644 index 000000000..e5363c06a --- /dev/null +++ b/src/frameworks/OpenGL/CGLAsyncPresent.inc @@ -0,0 +1,204 @@ +/* Bounded GPU-only staging: the producer never waits for display cadence. + * Three textures cover the currently displayed, pending, and writing frames. + * GPU fences order both the completed producer copy and subsequent reuse. */ + +/* darling-lol: software refresh period for the paced paths, from RCLOL_REFRESH_HZ + * (1-1000 Hz). Unset or out of range keeps the previous 60 Hz period. */ +static uint64_t nativeRefreshIntervalNS(void){ + static uint64_t interval; + if(!interval){ + const char *hz=getenv("RCLOL_REFRESH_HZ"); + long rate=hz?strtol(hz,NULL,10):0; + interval=(rate>0&&rate<=1000)?1000000000ull/(uint64_t)rate:16666667ull; + } + return interval; +} + +struct DarlingAsyncPresent { + CGLContextObj owner; + pthread_mutex_t mutex; + pthread_t thread; + int started, running, failed, displayed; + unsigned width, height; + GLuint textures[3], copy_fbos[2]; + int state[3]; /* 0 free, 1 pending, 2 display, 3 producer writing */ + GLsync produced[3], consumed[3]; + unsigned submissions, frames; + uint64_t serial[3]; +}; +static uint64_t nativeMonotonicNS(void) { + struct timespec now; + if(__lolsys_syscall(1,(long)&now,0,0,0,0,228))return 0; + return (uint64_t)now.tv_sec*1000000000ull+now.tv_nsec; +} +static void nativeWaitUntil(uint64_t deadline) { + struct timespec wake={deadline/1000000000ull,deadline%1000000000ull}; + while(__lolsys_syscall(1,1,(long)&wake,0,0,0,230)==-4){} +} +static void *asyncPresentWorker(void *opaque) { + struct DarlingAsyncPresent *a=opaque; + CGLContextObj ctx=a->owner; + int valid=p_eglBindAPI(EGL_OPENGL_API) && p_eglMakeCurrent(ctx->egl_display, + ctx->native_present_surface,ctx->native_present_surface,ctx->native_present_context); + GLuint fbo=0; + if(getenv("RCLOL_GPU_DIRECT_TRACE"))fprintf(stderr,"GPU_ASYNC worker_context=%d\n",valid); + /* The async context can wait for native VSync without blocking the + * Chromium producer. Keep the monotonic limiter as a fallback/bound. */ + int nativeVSync=0; + unsigned slowNativeSwaps=0; + if(valid){ + const char *vsync=getenv("RCLOL_GPU_DIRECT_VSYNC"); + nativeVSync=vsync && vsync[0]=='1' && p_eglSwapInterval(ctx->egl_display,1); + if(!nativeVSync)p_eglSwapInterval(ctx->egl_display,0); + glGenFramebuffers(1,&fbo); + } + uint64_t deadline=nativeMonotonicNS(); + for(;;){ + pthread_mutex_lock(&a->mutex); + int running=a->running,index=-1; + GLsync produced=NULL; + if(running && valid){ + for(int n=0;n<3;++n)if(a->state[n]==1 && a->produced[n] && (a->displayed<0 || a->serial[n]>a->serial[a->displayed])){ + GLenum ready=glClientWaitSync(a->produced[n],0,0); + if(ready==GL_WAIT_FAILED){valid=0;break;} + if((ready==GL_ALREADY_SIGNALED || ready==GL_CONDITION_SATISFIED) && + (index<0 || a->serial[n]>a->serial[index]))index=n; + } + if(valid && index>=0){ + for(int n=0;n<3;++n)if(a->state[n]==1 && a->serial[n]serial[index])a->state[n]=0; + a->state[index]=2;produced=a->produced[index];a->produced[index]=NULL; + } + } + pthread_mutex_unlock(&a->mutex); + if(!running || !valid)break; + DarlingNativePresentTarget *target=ctx->native_present_target; + if(!target || __atomic_load_n(&target->magic,__ATOMIC_ACQUIRE)!=DARLING_NATIVE_PRESENT_MAGIC || target->target!=ctx->native_present_window){if(produced)glDeleteSync(produced);valid=0;break;} + if(index>=0){ + uint64_t workStarted=nativeMonotonicNS(); + if(produced){glWaitSync(produced,0,GL_TIMEOUT_IGNORED);glDeleteSync(produced);} + glBindFramebuffer(GL_READ_FRAMEBUFFER,fbo); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,a->textures[index],0); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER,0);glDrawBuffer(GL_BACK);glReadBuffer(GL_COLOR_ATTACHMENT0); + glDisable(GL_SCISSOR_TEST);glDisable(GL_FRAMEBUFFER_SRGB); + if(glCheckFramebufferStatus(GL_READ_FRAMEBUFFER)!=GL_FRAMEBUFFER_COMPLETE){valid=0;break;} + glBlitFramebuffer(0,a->height,a->width,0,0,0,a->width,a->height,GL_COLOR_BUFFER_BIT,GL_NEAREST); + GLsync consumed=glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE,0);glFlush(); + int swapped=consumed && glGetError()==GL_NO_ERROR && p_eglSwapBuffers(ctx->egl_display,ctx->native_present_surface); + uint64_t workFinished=nativeMonotonicNS(); + /* Xwayland may acknowledge interval 1 but throttle an occluded + * surface to one swap per second. Two consecutive long swaps + * select our existing bounded 60 Hz clock for this surface. + * Keep texture ownership and the producer independent of it. */ + if(nativeVSync && swapped){ + slowNativeSwaps=workFinished-workStarted>=100000000ull?slowNativeSwaps+1:0; + if(slowNativeSwaps>=2 && p_eglSwapInterval(ctx->egl_display,0)){ + nativeVSync=0; + deadline=workFinished; + } + } + pthread_mutex_lock(&a->mutex); + a->consumed[index]=consumed; + if(a->displayed>=0)a->state[a->displayed]=0; + a->displayed=index; + if(swapped)++a->frames; + pthread_mutex_unlock(&a->mutex); + if(!swapped){valid=0;break;} + __atomic_store_n(&target->presenter,getpid(),__ATOMIC_RELEASE); + __atomic_fetch_add(&target->sequence,1,__ATOMIC_RELEASE); + __atomic_store_n(&target->ready,3,__ATOMIC_RELEASE); + __atomic_fetch_add(&target->reserved[3],(uint32_t)((workFinished-workStarted)/1000),__ATOMIC_RELAXED); + __atomic_store_n(&target->reserved[6],nativeVSync?0x544d4732u:0x544d4731u,__ATOMIC_RELEASE); + } + /* If the producer finishes just after our refresh deadline, wait for + * that frame in short increments instead of losing a whole refresh. + * Only a real swap advances the display clock. */ + if(index<0){__atomic_fetch_add(&target->reserved[1],1,__ATOMIC_RELAXED);nativeWaitUntil(nativeMonotonicNS()+1000000ull);continue;} + /* Native VSync owns the refresh cadence; avoid a second clock that + * can miss the compositor's next refresh after a variable swap wait. */ + if(nativeVSync)continue; + uint64_t refreshNS=nativeRefreshIntervalNS(); + deadline += refreshNS; + uint64_t now=nativeMonotonicNS(); + /* Preserve the refresh phase across small scheduling delays. Reset only + * after a full missed period, so lateness does not accumulate as drift. */ + if(now>deadline && now-deadline>refreshNS){__atomic_fetch_add(&target->reserved[2],1,__ATOMIC_RELAXED);deadline=now;} + nativeWaitUntil(deadline); + } + if(fbo)glDeleteFramebuffers(1,&fbo); + p_eglMakeCurrent(ctx->egl_display,EGL_NO_SURFACE,EGL_NO_SURFACE,EGL_NO_CONTEXT); + pthread_mutex_lock(&a->mutex);a->failed=!valid;a->running=0;pthread_mutex_unlock(&a->mutex); + if(ctx->native_present_target)__atomic_store_n(&ctx->native_present_target->ready,0,__ATOMIC_RELEASE); + return NULL; +} +static void stopAsyncPresent(CGLContextObj ctx) { + struct DarlingAsyncPresent *a=ctx->native_async; + if(!a)return; + pthread_mutex_lock(&a->mutex);a->running=0;pthread_mutex_unlock(&a->mutex); + if(a->started)pthread_join(a->thread,NULL); + CGLContextObj previous=CGLGetCurrentContext(); + if(p_eglMakeCurrent(ctx->egl_display,ctx->egl_surface,ctx->egl_surface,ctx->egl_context)){ + for(int n=0;n<3;++n){if(a->produced[n])glDeleteSync(a->produced[n]);if(a->consumed[n])glDeleteSync(a->consumed[n]);} + glDeleteFramebuffers(2,a->copy_fbos);glDeleteTextures(3,a->textures); + if(previous && previous!=ctx)p_eglMakeCurrent(previous->egl_display,previous->egl_surface,previous->egl_surface,previous->egl_context); + else if(!previous)p_eglMakeCurrent(ctx->egl_display,EGL_NO_SURFACE,EGL_NO_SURFACE,EGL_NO_CONTEXT); + } + pthread_mutex_destroy(&a->mutex);free(a);ctx->native_async=NULL; +} +static int enqueueAsyncPresent(CGLContextObj ctx,GLuint texture,GLenum textureTarget,unsigned width,unsigned height) { + struct DarlingAsyncPresent *a=ctx->native_async; + if(a && (a->width!=width || a->height!=height)){stopAsyncPresent(ctx);a=NULL;} + if(!a){ + a=calloc(1,sizeof(*a));if(!a)return 0; + a->owner=ctx;a->width=width;a->height=height;a->displayed=-1; + pthread_mutex_init(&a->mutex,NULL); + GLint oldTexture;glGetIntegerv(GL_TEXTURE_BINDING_2D,&oldTexture); + glGenFramebuffers(2,a->copy_fbos);glGenTextures(3,a->textures); + for(int n=0;n<3;++n){glBindTexture(GL_TEXTURE_2D,a->textures[n]);glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA8,width,height,0,GL_RGBA,GL_UNSIGNED_BYTE,NULL);} + glBindTexture(GL_TEXTURE_2D,oldTexture);ctx->native_async=a; + GLenum allocationError=glGetError();if(allocationError!=GL_NO_ERROR){if(getenv("RCLOL_GPU_DIRECT_TRACE"))fprintf(stderr,"GPU_ASYNC allocation_error=%u\n",allocationError);stopAsyncPresent(ctx);return 0;} + a->running=1; + pthread_attr_t attr;pthread_attr_init(&attr);pthread_attr_setstacksize(&attr,1024*1024); + int status=pthread_create(&a->thread,&attr,asyncPresentWorker,a);pthread_attr_destroy(&attr); + if(getenv("RCLOL_GPU_DIRECT_TRACE"))fprintf(stderr,"GPU_ASYNC worker_create=%d\n",status); + if(status){stopAsyncPresent(ctx);return 0;}a->started=1; + } + pthread_mutex_lock(&a->mutex); + if(a->failed || !a->running){pthread_mutex_unlock(&a->mutex);return 0;} + int index=-1; + for(int n=0;n<3;++n)if(!a->state[n]){index=n;break;} + /* Keep an older pending frame available to finish while replacing the + * newest pending frame with fresh producer work. Never touch display slots. */ + if(index<0)for(int n=0;n<3;++n)if(a->state[n]==1 && + (index<0 || a->serial[n]>a->serial[index]))index=n; + if(index<0){pthread_mutex_unlock(&a->mutex);return 0;} + a->state[index]=3; + GLsync consumed=a->consumed[index],produced=a->produced[index]; + a->consumed[index]=a->produced[index]=NULL; + pthread_mutex_unlock(&a->mutex); + if(consumed){glWaitSync(consumed,0,GL_TIMEOUT_IGNORED);glDeleteSync(consumed);} + if(produced)glDeleteSync(produced); + GLint oldRead,oldDraw; + glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING,&oldRead);glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING,&oldDraw); + GLboolean scissor=glIsEnabled(GL_SCISSOR_TEST),srgb=glIsEnabled(GL_FRAMEBUFFER_SRGB); + glDisable(GL_SCISSOR_TEST);glDisable(GL_FRAMEBUFFER_SRGB); + glBindFramebuffer(GL_READ_FRAMEBUFFER,a->copy_fbos[0]); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,textureTarget,texture,0);glReadBuffer(GL_COLOR_ATTACHMENT0); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER,a->copy_fbos[1]); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,a->textures[index],0);glDrawBuffer(GL_COLOR_ATTACHMENT0); + int complete=glCheckFramebufferStatus(GL_READ_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE && glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE; + if(complete)glBlitFramebuffer(0,0,width,height,0,0,width,height,GL_COLOR_BUFFER_BIT,GL_NEAREST); + glBindFramebuffer(GL_READ_FRAMEBUFFER,oldRead);glBindFramebuffer(GL_DRAW_FRAMEBUFFER,oldDraw); + if(scissor)glEnable(GL_SCISSOR_TEST);if(srgb)glEnable(GL_FRAMEBUFFER_SRGB); + produced=glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE,0);glFlush(); + GLenum copyError=glGetError(); + if(!complete || !produced || copyError!=GL_NO_ERROR){if(getenv("RCLOL_GPU_DIRECT_TRACE"))fprintf(stderr,"GPU_ASYNC copy_error=%u fence=%d\n",copyError,produced!=NULL);if(produced)glDeleteSync(produced);pthread_mutex_lock(&a->mutex);a->state[index]=0;pthread_mutex_unlock(&a->mutex);return 0;} + pthread_mutex_lock(&a->mutex); + a->produced[index]=produced;a->state[index]=1;a->serial[index]=++a->submissions; + if(ctx->native_present_target)__atomic_fetch_add(&ctx->native_present_target->reserved[0],1,__ATOMIC_RELAXED); + if(ctx->native_present_target){ + __atomic_store_n(&ctx->native_present_target->reserved[4],__atomic_load_n(&nativeUploadCount,__ATOMIC_RELAXED),__ATOMIC_RELAXED); + __atomic_store_n(&ctx->native_present_target->reserved[5],__atomic_load_n(&nativeUploadMicroseconds,__ATOMIC_RELAXED),__ATOMIC_RELAXED); + } + pthread_mutex_unlock(&a->mutex); + return 1; +} diff --git a/src/frameworks/OpenGL/include/OpenGL/DarlingNativePresent.h b/src/frameworks/OpenGL/include/OpenGL/DarlingNativePresent.h new file mode 100644 --- /dev/null +++ b/src/frameworks/OpenGL/include/OpenGL/DarlingNativePresent.h @@ -0,0 +1,11 @@ +#ifndef DARLING_NATIVE_PRESENT_H +#define DARLING_NATIVE_PRESENT_H +#include +#define DARLING_NATIVE_PRESENT_MAGIC 0x47505532u +/* Host X11 target metadata only; no pixels, input, or account information. */ +typedef struct { + uint32_t magic,target,width,height; + uint32_t ready,presenter,sequence,visual; + uint32_t flags,reserved[7]; +} DarlingNativePresentTarget; +#endif diff --git a/src/startup/mldr/commpage-cpu-caps.h b/src/startup/mldr/commpage-cpu-caps.h new file mode 100644 --- /dev/null +++ b/src/startup/mldr/commpage-cpu-caps.h @@ -0,0 +1,41 @@ +#ifndef MLDR_COMMPAGE_CPU_CAPS_H +#define MLDR_COMMPAGE_CPU_CAPS_H +#include + +/* CPUID leaf 1 uses EDX for legacy SIMD and ECX for newer SIMD. AVX + * also needs the operating system to save both XMM and YMM state. */ +static inline uint64_t commpage_decode_cpu_caps(uint32_t ecx1, uint32_t edx1, + uint32_t ebx7, uint64_t xcr0, int long_mode) +{ + uint64_t caps = long_mode ? k64Bit : 0; +#define FEATURE(reg, bit, cap) do { if ((reg) & (UINT32_C(1) << (bit))) caps |= (cap); } while (0) + FEATURE(edx1, 23, kHasMMX); + FEATURE(edx1, 25, kHasSSE); + FEATURE(edx1, 26, kHasSSE2); + FEATURE(ecx1, 0, kHasSSE3); + FEATURE(ecx1, 9, kHasSupplementalSSE3); + FEATURE(ecx1, 19, kHasSSE4_1); + FEATURE(ecx1, 20, kHasSSE4_2); + FEATURE(ecx1, 25, kHasAES); + FEATURE(ecx1, 30, kHasRDRAND); + const uint32_t avx_requirements = (UINT32_C(1) << 26) | (UINT32_C(1) << 27) | (UINT32_C(1) << 28); + const int avx_usable = (ecx1 & avx_requirements) == avx_requirements && (xcr0 & 6) == 6; + if (avx_usable) { + caps |= kHasAVX1_0; + FEATURE(ecx1, 12, kHasFMA); + FEATURE(ecx1, 29, kHasF16C); + FEATURE(ebx7, 5, kHasAVX2_0); + } + FEATURE(ebx7, 9, kHasENFSTRG); + FEATURE(ebx7, 3, kHasBMI1); + FEATURE(ebx7, 8, kHasBMI2); + FEATURE(ebx7, 11, kHasRTM); + FEATURE(ebx7, 4, kHasHLE); + FEATURE(ebx7, 18, kHasRDSEED); + FEATURE(ebx7, 19, kHasADX); + FEATURE(ebx7, 14, kHasMPX); + FEATURE(ebx7, 2, kHasSGX); +#undef FEATURE + return caps; +} +#endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5c924b34d..54f134873 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -119,6 +119,13 @@ SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_ SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") set(CMAKE_OBJC_FLAGS "${CMAKE_OBJC_FLAGS} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") +# Recent clang compiles an empty @[] / @{} for a macOS 11+ runtime into a reference to +# __NSArray0__struct / __NSDictionary0__struct, which Darling's CoreFoundation does not define +# (it provides __NSArray0__ / __NSDictionary0__). Keep the earlier code generation. +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fobjc-runtime=macosx-10.15") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fobjc-runtime=macosx-10.15") +set(CMAKE_OBJC_FLAGS "${CMAKE_OBJC_FLAGS} -fobjc-runtime=macosx-10.15") + add_definitions(-target ${APPLE_TARGET_TRIPLET_PRIMARY}) include(darling_lib) include(darling_static_lib) diff --git a/src/libelfloader/wrapgen/wrapgen.cpp b/src/libelfloader/wrapgen/wrapgen.cpp index 7079e613e..397ec6de0 100644 --- a/src/libelfloader/wrapgen/wrapgen.cpp +++ b/src/libelfloader/wrapgen/wrapgen.cpp @@ -268,7 +268,10 @@ void generate_wrapper(std::ofstream& output, const char* soname, const std::set< for (const std::string& sym : symbols) { - output << "void* " << sym << "() {\n" + // ld64's x86-64 resolver stub saves integer argument registers only. + // ELF lookup can clobber XMM arguments before the stub jumps to its + // resolved function. Preserve them in the resolver itself. + output << "__attribute__((preserve_all)) void* " << sym << "() {\n" "\t__asm__(\".symbol_resolver _" << sym << "\");\n" "\treturn _elfcalls->dlsym_fatal(lib_handle, \"" << sym << "\");\n" "}\n\n"; diff --git a/src/frameworks/CoreServices/src/LaunchServices/LSInfo.m b/src/frameworks/CoreServices/src/LaunchServices/LSInfo.m index 4cb2a4941..29eef9956 100644 --- a/src/frameworks/CoreServices/src/LaunchServices/LSInfo.m +++ b/src/frameworks/CoreServices/src/LaunchServices/LSInfo.m @@ -20,6 +20,7 @@ along with Darling. If not, see . #include #include +#include "LSHostOpen.h" #include #include #import @@ -106,7 +107,7 @@ CFArrayRef LSCopyApplicationURLsForBundleIdentifier(CFStringRef inBundleIdentifi { if (!inBundleIdentifier) { - if (*outError) + if (outError) *outError = CFErrorCreate(NULL, kCFErrorDomainOSStatus, paramErr, NULL); return NULL; } @@ -116,7 +117,7 @@ CFArrayRef LSCopyApplicationURLsForBundleIdentifier(CFStringRef inBundleIdentifi if (dq == nil) { - if (*outError) + if (outError) *outError = CFErrorCreate(NULL, kCFErrorDomainOSStatus, fnfErr, NULL); return NULL; } @@ -138,7 +139,7 @@ CFArrayRef LSCopyApplicationURLsForBundleIdentifier(CFStringRef inBundleIdentifi } else { - if (*outError) + if (outError) *outError = CFErrorCreate(NULL, kCFErrorDomainOSStatus, kLSApplicationNotFoundErr, NULL); retval = NULL; } @@ -247,7 +248,7 @@ CFURLRef LSCopyDefaultApplicationURLForContentType(CFStringRef inContentType, LS if (dq == nil) { - if (*outError) + if (outError) *outError = CFErrorCreate(NULL, kCFErrorDomainOSStatus, fnfErr, NULL); return NULL; } @@ -275,7 +276,7 @@ CFURLRef LSCopyDefaultApplicationURLForContentType(CFStringRef inContentType, LS } else { - if (*outError) + if (outError) *outError = CFErrorCreate(NULL, kCFErrorDomainOSStatus, kLSApplicationNotFoundErr, NULL); retval = NULL; } @@ -292,7 +293,7 @@ CFURLRef LSCopyDefaultApplicationURLForURL(CFURLRef inURL, LSRolesMask inRoleMas if (status != noErr) { - if (*outError) + if (outError) *outError = CFErrorCreate(NULL, kCFErrorDomainOSStatus, status, NULL); return NULL; } @@ -306,6 +307,9 @@ CFURLRef LSCopyDefaultApplicationURLForURL(CFURLRef inURL, LSRolesMask inRoleMas OSStatus LSGetApplicationForURL(CFURLRef inURL, LSRolesMask inRoleMask, FSRef *outAppRef, CFURLRef *outAppURL) { + if (_LSGetHostHandlerForURL(inURL, outAppRef, outAppURL) == noErr) + return noErr; + NSString* extension = [(NSURL*) inURL pathExtension]; return LSGetApplicationForInfo(kLSUnknownType, kLSUnknownCreator, (CFStringRef) extension, inRoleMask, outAppRef, outAppURL); } diff --git a/src/frameworks/CoreServices/src/LaunchServices/CMakeLists.txt b/src/frameworks/CoreServices/src/LaunchServices/CMakeLists.txt index bb7f35068..957cc9d3e 100644 --- a/src/frameworks/CoreServices/src/LaunchServices/CMakeLists.txt +++ b/src/frameworks/CoreServices/src/LaunchServices/CMakeLists.txt @@ -1,5 +1,8 @@ set(SUBFRAMEWORK_VERSION "A") +# LSHostOpen.c loads the host's libsystemd through the ELF loader. +include_directories(${CMAKE_SOURCE_DIR}/src/startup/mldr/elfcalls) + generate_sdk_subframework(LaunchServices BASE_PATH ${coreservices_sdk_path} VERSION ${SUBFRAMEWORK_VERSION} @@ -20,6 +23,8 @@ add_framework(LaunchServices LSApplicationWorkspace.m LSRunning.m constants.c + LSHostOpen.c + LSHostOpenBus.c LSInfo.m LSOpen.m UTType.m diff --git a/src/frameworks/CoreServices/src/LaunchServices/LSHostOpen.h b/src/frameworks/CoreServices/src/LaunchServices/LSHostOpen.h new file mode 100644 --- /dev/null +++ b/src/frameworks/CoreServices/src/LaunchServices/LSHostOpen.h @@ -0,0 +1,43 @@ +/* +This file is part of Darling. + +Copyright (C) 2026 Darling developers + +Darling is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Darling is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Darling. If not, see . +*/ + +#ifndef LS_HOST_OPEN_H_ +#define LS_HOST_OPEN_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* URLs with these schemes (http, https, mailto) are opened by the host desktop. */ +Boolean _LSIsHostOpenURL(CFURLRef url); + +/* Hands the URL to the host desktop. */ +OSStatus _LSOpenURLOnHost(CFURLRef url); + +/* Reports the host opener as the default application for a host-opened URL. */ +OSStatus _LSGetHostHandlerForURL(CFURLRef url, FSRef* outAppRef, CFURLRef* outAppURL); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/frameworks/CoreServices/src/LaunchServices/LSHostOpen.c b/src/frameworks/CoreServices/src/LaunchServices/LSHostOpen.c new file mode 100644 --- /dev/null +++ b/src/frameworks/CoreServices/src/LaunchServices/LSHostOpen.c @@ -0,0 +1,207 @@ +/* +This file is part of Darling. + +Copyright (C) 2026 Darling developers + +Darling is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Darling is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Darling. If not, see . +*/ + +/* + * Web and mail URLs have no handler inside the prefix, so they are handed to + * the Linux desktop. sd-bus is used from the host's libsystemd, loaded through + * the ELF loader; the guest shares the host's user and network, so the + * session bus is reachable directly. + */ + +#include "LSHostOpen.h" +#include "LSHostOpenBus.h" +#include +#include +#include +#include +#include +#include + +extern struct elf_calls* _elfcalls; + +/* Where the host's root file system appears inside the prefix. */ +#define HOST_ROOT "/Volumes/SystemRoot" + +static struct ls_host_api host_api; +static Boolean host_api_ready; +static pthread_once_t host_api_once = PTHREAD_ONCE_INIT; + +static void load_host_api(void) +{ + void *systemd, *libc; + struct ls_host_api api; + + if (_elfcalls == NULL) + return; + + systemd = _elfcalls->dlopen("libsystemd.so.0"); + if (systemd == NULL) + return; + libc = _elfcalls->dlopen("libc.so.6"); + if (libc == NULL) + { + _elfcalls->dlclose(systemd); + return; + } + + memset(&api, 0, sizeof(api)); +#define BIND(lib, field) \ + if ((*(void**) &api.field = _elfcalls->dlsym(lib, #field)) == NULL) \ + goto fail; + BIND(systemd, sd_bus_new); + BIND(systemd, sd_bus_set_address); + BIND(systemd, sd_bus_set_bus_client); + BIND(systemd, sd_bus_start); + BIND(systemd, sd_bus_flush_close_unref); + BIND(systemd, sd_bus_message_new_method_call); + BIND(systemd, sd_bus_message_append); + BIND(systemd, sd_bus_message_append_strv); + BIND(systemd, sd_bus_message_open_container); + BIND(systemd, sd_bus_message_close_container); + BIND(systemd, sd_bus_call); + BIND(systemd, sd_bus_message_unref); + BIND(systemd, sd_bus_error_free); + /* Host IDs and paths, not the prefix's view of them. */ + BIND(libc, getuid); + BIND(libc, access); +#undef BIND + api.getenv = getenv; + api.runtime_dir_root = "/run/user"; + + host_api = api; + host_api_ready = TRUE; + return; + +fail: + _elfcalls->dlclose(libc); + _elfcalls->dlclose(systemd); +} + +static const struct ls_host_api* get_host_api(void) +{ + pthread_once(&host_api_once, load_host_api); + return host_api_ready ? &host_api : NULL; +} + +/* Returns the absolute URL as a malloc'd UTF-8 string. */ +static char* copy_url_string(CFURLRef url) +{ + CFURLRef absolute; + CFStringRef string; + CFIndex size; + char* buffer = NULL; + + if (url == NULL) + return NULL; + absolute = CFURLCopyAbsoluteURL(url); + if (absolute == NULL) + return NULL; + + string = CFURLGetString(absolute); + size = CFStringGetMaximumSizeForEncoding(CFStringGetLength(string), kCFStringEncodingUTF8) + 1; + if (size <= LS_HOST_OPEN_MAX_URL_LENGTH * 4 + 1) + { + buffer = malloc(size); + if (buffer != NULL && !CFStringGetCString(string, buffer, size, kCFStringEncodingUTF8)) + { + free(buffer); + buffer = NULL; + } + } + CFRelease(absolute); + return buffer; +} + +Boolean _LSIsHostOpenURL(CFURLRef url) +{ + CFStringRef scheme; + Boolean result = FALSE; + + if (url == NULL) + return FALSE; + scheme = CFURLCopyScheme(url); + if (scheme == NULL) + return FALSE; + + result = CFStringCompare(scheme, CFSTR("http"), kCFCompareCaseInsensitive) == kCFCompareEqualTo + || CFStringCompare(scheme, CFSTR("https"), kCFCompareCaseInsensitive) == kCFCompareEqualTo + || CFStringCompare(scheme, CFSTR("mailto"), kCFCompareCaseInsensitive) == kCFCompareEqualTo; + CFRelease(scheme); + return result; +} + +OSStatus _LSOpenURLOnHost(CFURLRef url) +{ + const struct ls_host_api* api; + char* string = copy_url_string(url); + int r; + + if (string == NULL || _LSHostOpenValidateURL(string) < 0) + { + free(string); + return paramErr; + } + + api = get_host_api(); + if (api == NULL) + { + fprintf(stderr, "LaunchServices: cannot open URL: host libsystemd is unavailable\n"); + free(string); + return kLSApplicationNotFoundErr; + } + + r = _LSHostOpenURL(api, string, NULL); + free(string); + if (r < 0) + { + fprintf(stderr, "LaunchServices: host desktop did not accept URL (error %d)\n", -r); + return kLSServerCommunicationErr; + } + return noErr; +} + +OSStatus _LSGetHostHandlerForURL(CFURLRef url, FSRef* outAppRef, CFURLRef* outAppURL) +{ + const struct ls_host_api* api; + const char* opener; + char path[256]; + + if (!_LSIsHostOpenURL(url)) + return kLSApplicationNotFoundErr; + api = get_host_api(); + if (api == NULL) + return kLSApplicationNotFoundErr; + + opener = _LSHostOpenFindOpener(api); + snprintf(path, sizeof(path), HOST_ROOT "%s", opener ? opener : "/usr/bin/xdg-open"); + + if (outAppRef != NULL) + { + OSStatus status = FSPathMakeRef((const UInt8*) path, outAppRef, NULL); + if (status != noErr) + return status; + } + if (outAppURL != NULL) + { + *outAppURL = CFURLCreateFromFileSystemRepresentation(NULL, (const UInt8*) path, strlen(path), FALSE); + if (*outAppURL == NULL) + return kLSApplicationNotFoundErr; + } + return noErr; +} diff --git a/src/frameworks/CoreServices/src/LaunchServices/LSHostOpenBus.h b/src/frameworks/CoreServices/src/LaunchServices/LSHostOpenBus.h new file mode 100644 --- /dev/null +++ b/src/frameworks/CoreServices/src/LaunchServices/LSHostOpenBus.h @@ -0,0 +1,111 @@ +/* +This file is part of Darling. + +Copyright (C) 2026 Darling developers + +Darling is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Darling is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Darling. If not, see . +*/ + +/* + * Hands a URL to the host desktop over the user's D-Bus session bus. + * + * This part has no Darling or CoreFoundation dependencies: every host + * function comes from the table below, so it can also be built natively on + * Linux against libsystemd for testing. Errors are negative Linux errno + * values as returned by sd-bus. + */ + +#ifndef LS_HOST_OPEN_BUS_H_ +#define LS_HOST_OPEN_BUS_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define LS_HOST_OPEN_MAX_URL_LENGTH 4096 + +/* Linux errno values; the guest's errno numbering differs. */ +#define LS_HOST_LINUX_ENOENT 2 +#define LS_HOST_LINUX_EINVAL 22 +#define LS_HOST_LINUX_ENOSYS 38 +#define LS_HOST_LINUX_ETIMEDOUT 110 + +/* Layout of sd_bus_error. */ +struct ls_host_bus_error +{ + const char* name; + const char* message; + int _need_free; +}; + +struct ls_host_api +{ + /* libsystemd (sd-bus); bus and message objects are opaque */ + int (*sd_bus_new)(void** ret); + int (*sd_bus_set_address)(void* bus, const char* address); + int (*sd_bus_set_bus_client)(void* bus, int b); + int (*sd_bus_start)(void* bus); + void* (*sd_bus_flush_close_unref)(void* bus); + int (*sd_bus_message_new_method_call)(void* bus, void** m, const char* destination, + const char* path, const char* interface, const char* member); + int (*sd_bus_message_append)(void* m, const char* types, ...); + int (*sd_bus_message_append_strv)(void* m, char** l); + int (*sd_bus_message_open_container)(void* m, char type, const char* contents); + int (*sd_bus_message_close_container)(void* m); + int (*sd_bus_call)(void* bus, void* m, uint64_t usec, struct ls_host_bus_error* ret_error, void** reply); + void* (*sd_bus_message_unref)(void* m); + void (*sd_bus_error_free)(struct ls_host_bus_error* e); + + /* Host process environment */ + char* (*getenv)(const char* name); + unsigned int (*getuid)(void); + int (*access)(const char* path, int mode); + + /* Parent of the per-user runtime directories, normally "/run/user" */ + const char* runtime_dir_root; + /* Per-call timeout; 0 selects the default */ + uint64_t timeout_usec; +}; + +enum +{ + LS_HOST_OPEN_VIA_PORTAL = 1, + LS_HOST_OPEN_VIA_SYSTEMD = 2, +}; + +/* 1 if the URL has a scheme that is handed to the host (http, https, mailto). */ +int _LSHostOpenSchemeSupported(const char* url); + +/* 0 if the URL may be handed to the host, otherwise -EINVAL. */ +int _LSHostOpenValidateURL(const char* url); + +/* Host path of xdg-open, or NULL if it is not in the usual locations. */ +const char* _LSHostOpenFindOpener(const struct ls_host_api* api); + +/* + * Opens the URL through org.freedesktop.portal.OpenURI, or, if that fails, + * runs xdg-open in a transient systemd user service. Either way the handler + * is started by the host session, never as a child of the caller. + * Returns 0 and stores the method used in *method (may be NULL), or a + * negative errno. + */ +int _LSHostOpenURL(const struct ls_host_api* api, const char* url, int* method); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/frameworks/CoreServices/src/LaunchServices/LSHostOpenBus.c b/src/frameworks/CoreServices/src/LaunchServices/LSHostOpenBus.c new file mode 100644 --- /dev/null +++ b/src/frameworks/CoreServices/src/LaunchServices/LSHostOpenBus.c @@ -0,0 +1,278 @@ +/* +This file is part of Darling. + +Copyright (C) 2026 Darling developers + +Darling is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Darling is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Darling. If not, see . +*/ + +#include "LSHostOpenBus.h" +#include +#include +#include +#include +#include + +#define DEFAULT_TIMEOUT_USEC (10ULL * 1000 * 1000) +#define HOST_X_OK 1 + +static const char* const supported_schemes[] = { "http:", "https:", "mailto:" }; + +/* Passed to the transient unit so xdg-open finds the user's display. */ +static const char* const forwarded_environment[] = { "DISPLAY", "WAYLAND_DISPLAY", "XDG_CURRENT_DESKTOP" }; + +int _LSHostOpenSchemeSupported(const char* url) +{ + if (url == NULL) + return 0; + for (size_t i = 0; i < sizeof(supported_schemes) / sizeof(supported_schemes[0]); i++) + { + size_t len = strlen(supported_schemes[i]); + if (strncasecmp(url, supported_schemes[i], len) == 0) + return 1; + } + return 0; +} + +int _LSHostOpenValidateURL(const char* url) +{ + size_t len; + + if (!_LSHostOpenSchemeSupported(url)) + return -LS_HOST_LINUX_EINVAL; + + len = strlen(url); + if (len > LS_HOST_OPEN_MAX_URL_LENGTH) + return -LS_HOST_LINUX_EINVAL; + + for (size_t i = 0; i < len; i++) + { + unsigned char c = (unsigned char) url[i]; + if (c < 0x20 || c == 0x7f) + return -LS_HOST_LINUX_EINVAL; + } + return 0; +} + +const char* _LSHostOpenFindOpener(const struct ls_host_api* api) +{ + static const char* const candidates[] = { + "/usr/local/bin/xdg-open", + "/usr/bin/xdg-open", + "/bin/xdg-open", + }; + + if (api == NULL || api->access == NULL) + return NULL; + for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) + { + if (api->access(candidates[i], HOST_X_OK) == 0) + return candidates[i]; + } + return NULL; +} + +static int api_complete(const struct ls_host_api* api) +{ + return api != NULL && api->sd_bus_new && api->sd_bus_set_address && api->sd_bus_set_bus_client + && api->sd_bus_start && api->sd_bus_flush_close_unref && api->sd_bus_message_new_method_call + && api->sd_bus_message_append && api->sd_bus_message_append_strv + && api->sd_bus_message_open_container && api->sd_bus_message_close_container + && api->sd_bus_call && api->sd_bus_message_unref && api->sd_bus_error_free + && api->getenv && api->getuid; +} + +static int open_session_bus(const struct ls_host_api* api, void** ret) +{ + char address[512]; + const char* env = api->getenv("DBUS_SESSION_BUS_ADDRESS"); + void* bus = NULL; + int r; + + if (env != NULL && env[0] != '\0') + { + if (strlen(env) >= sizeof(address)) + return -LS_HOST_LINUX_EINVAL; + strcpy(address, env); + } + else + { + const char* root = api->runtime_dir_root ? api->runtime_dir_root : "/run/user"; + if (snprintf(address, sizeof(address), "unix:path=%s/%u/bus", root, api->getuid()) + >= (int) sizeof(address)) + return -LS_HOST_LINUX_EINVAL; + } + + r = api->sd_bus_new(&bus); + if (r < 0) + return r; + r = api->sd_bus_set_address(bus, address); + if (r >= 0) + r = api->sd_bus_set_bus_client(bus, 1); + if (r >= 0) + r = api->sd_bus_start(bus); + if (r < 0) + { + api->sd_bus_flush_close_unref(bus); + return r; + } + *ret = bus; + return 0; +} + +static int call(const struct ls_host_api* api, void* bus, void* m) +{ + struct ls_host_bus_error error = { NULL, NULL, 0 }; + void* reply = NULL; + uint64_t timeout = api->timeout_usec ? api->timeout_usec : DEFAULT_TIMEOUT_USEC; + int r = api->sd_bus_call(bus, m, timeout, &error, &reply); + + if (reply != NULL) + api->sd_bus_message_unref(reply); + api->sd_bus_error_free(&error); + return r < 0 ? r : 0; +} + +static int open_via_portal(const struct ls_host_api* api, void* bus, const char* url) +{ + void* m = NULL; + int r = api->sd_bus_message_new_method_call(bus, &m, "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", "org.freedesktop.portal.OpenURI", "OpenURI"); + if (r < 0) + return r; + + r = api->sd_bus_message_append(m, "ss", "", url); + if (r >= 0) + r = api->sd_bus_message_append(m, "a{sv}", 0); + if (r >= 0) + r = call(api, bus, m); + api->sd_bus_message_unref(m); + return r; +} + +static int append_environment(const struct ls_host_api* api, void* m) +{ + char storage[3][1024]; + char* entries[4]; + size_t count = 0; + int r; + + for (size_t i = 0; i < sizeof(forwarded_environment) / sizeof(forwarded_environment[0]); i++) + { + const char* value = api->getenv(forwarded_environment[i]); + int n; + + if (value == NULL || value[0] == '\0') + continue; + n = snprintf(storage[count], sizeof(storage[count]), "%s=%s", forwarded_environment[i], value); + if (n < 0 || n >= (int) sizeof(storage[count])) + continue; + entries[count] = storage[count]; + count++; + } + entries[count] = NULL; + if (count == 0) + return 0; + + r = api->sd_bus_message_open_container(m, 'r', "sv"); + if (r >= 0) + r = api->sd_bus_message_append(m, "s", "Environment"); + if (r >= 0) + r = api->sd_bus_message_open_container(m, 'v', "as"); + if (r >= 0) + r = api->sd_bus_message_append_strv(m, entries); + if (r >= 0) + r = api->sd_bus_message_close_container(m); + if (r >= 0) + r = api->sd_bus_message_close_container(m); + return r; +} + +static int open_via_systemd(const struct ls_host_api* api, void* bus, const char* url) +{ + static unsigned int counter; + struct timespec now; + char unit[128]; + const char* opener = _LSHostOpenFindOpener(api); + void* m = NULL; + int r; + + /* Bare names are resolved by systemd >= 248. */ + if (opener == NULL) + opener = "xdg-open"; + + clock_gettime(CLOCK_REALTIME, &now); + snprintf(unit, sizeof(unit), "app-darling-openurl-%d-%llx-%u.service", (int) getpid(), + (unsigned long long) now.tv_sec * 1000000000ULL + (unsigned long long) now.tv_nsec, + __atomic_add_fetch(&counter, 1, __ATOMIC_RELAXED)); + + r = api->sd_bus_message_new_method_call(bus, &m, "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager", "StartTransientUnit"); + if (r < 0) + return r; + + r = api->sd_bus_message_append(m, "ss", unit, "fail"); + if (r >= 0) + r = api->sd_bus_message_open_container(m, 'a', "(sv)"); + if (r >= 0) + r = api->sd_bus_message_append(m, "(sv)(sv)(sv)(sv)", + "Description", "s", "Open URL from Darling", + /* Keep the handler running after xdg-open exits. */ + "KillMode", "s", "process", + "CollectMode", "s", "inactive-or-failed", + "ExecStart", "a(sasb)", 1, opener, 2, "xdg-open", url, 0); + if (r >= 0) + r = append_environment(api, m); + if (r >= 0) + r = api->sd_bus_message_close_container(m); + if (r >= 0) + r = api->sd_bus_message_append(m, "a(sa(sv))", 0); + if (r >= 0) + r = call(api, bus, m); + api->sd_bus_message_unref(m); + return r; +} + +int _LSHostOpenURL(const struct ls_host_api* api, const char* url, int* method) +{ + void* bus = NULL; + int r; + + r = _LSHostOpenValidateURL(url); + if (r < 0) + return r; + if (!api_complete(api)) + return -LS_HOST_LINUX_ENOSYS; + + r = open_session_bus(api, &bus); + if (r < 0) + return r; + + r = open_via_portal(api, bus, url); + if (r >= 0) + { + if (method) + *method = LS_HOST_OPEN_VIA_PORTAL; + } + /* A portal that timed out may still open the URL; don't open it twice. */ + else if (r != -LS_HOST_LINUX_ETIMEDOUT) + { + r = open_via_systemd(api, bus, url); + if (r >= 0 && method) + *method = LS_HOST_OPEN_VIA_SYSTEMD; + } + + api->sd_bus_flush_close_unref(bus); + return r < 0 ? r : 0; +} diff --git a/src/frameworks/CoreServices/src/CarbonCore/Gestalt.cpp b/src/frameworks/CoreServices/src/CarbonCore/Gestalt.cpp index dffdd4cb2..7430f587b 100644 --- a/src/frameworks/CoreServices/src/CarbonCore/Gestalt.cpp +++ b/src/frameworks/CoreServices/src/CarbonCore/Gestalt.cpp @@ -25,6 +25,9 @@ along with Darling. If not, see . OSStatus Gestalt(uint32_t type, int* value) { + if (!value) + return paramErr; + *value = 0; switch (type) { case gestaltSystemVersion: @@ -32,24 +35,31 @@ OSStatus Gestalt(uint32_t type, int* value) case gestaltSystemVersionMinor: case gestaltSystemVersionPatch: { - // We may be mixing the Darwin and OS version, but this API is deprecated anyway - int name[2] = { CTL_KERN, KERN_OSRELEASE }; - char version[32]; - size_t version_len = sizeof(version) - 1; - int maj, min, patch; - - sysctl(name, 2, version, &version_len, nullptr, 0); - sscanf(version, "%d.%d.%d", &maj, &min, &patch); - - if (type == gestaltSystemVersion) - *value = 0x100 * maj + 0x10 * min; - else if (type == gestaltSystemVersionMajor) - *value = maj; - else if (type == gestaltSystemVersionMinor) - *value = min; - else if (type == gestaltSystemVersionPatch) - *value = patch; - } + // Darwin's kernel release is not the macOS product version. + char version[64] = {}; + size_t version_len = sizeof(version); + int major = 0, minor = 0, patch = 0; + if (sysctlbyname("kern.osproductversion", version, &version_len, nullptr, 0) != 0) + return gestaltUnknownErr; + version[sizeof(version) - 1] = '\0'; + if (sscanf(version, "%d.%d.%d", &major, &minor, &patch) < 1 || + major < 0 || major > 99 || minor < 0 || patch < 0) + return gestaltUnknownErr; + + if (type == gestaltSystemVersion) { + // Legacy packed BCD: minor and bug-fix components saturate at 9. + const int packedMajor = ((major / 10) << 4) | (major % 10); + *value = (packedMajor << 8) | ((minor > 9 ? 9 : minor) << 4) | + (patch > 9 ? 9 : patch); + } else if (type == gestaltSystemVersionMajor) + *value = major; + else if (type == gestaltSystemVersionMinor) + *value = minor; + else + *value = patch; + // Version selectors must not fall through to the architecture case. + return 0; + } case gestaltSysArchitecture: #ifdef __powerpc__ *value = gestaltPowerPC; diff --git a/src/frameworks/Accelerate/vecLib/vDSP/CMakeLists.txt b/src/frameworks/Accelerate/vecLib/vDSP/CMakeLists.txt index 6ef701609..9bdcace68 100644 --- a/src/frameworks/Accelerate/vecLib/vDSP/CMakeLists.txt +++ b/src/frameworks/Accelerate/vecLib/vDSP/CMakeLists.txt @@ -7,7 +7,11 @@ set(DYLIB_CURRENT_VERSION "671.250.4") add_darling_library(vDSP SHARED src/vDSP.c src/extrema.c + src/arith.c + src/fft.c ) +# WebAudio calls these on the realtime render thread. +target_compile_options(vDSP PRIVATE -O2) make_fat(vDSP) target_link_libraries(vDSP system) install(TARGETS vDSP DESTINATION libexec/darling/usr/lib) diff --git a/src/frameworks/Accelerate/vecLib/vDSP/include/vDSP/vDSP.h b/src/frameworks/Accelerate/vecLib/vDSP/include/vDSP/vDSP.h index c6b437c5b..88505eda4 100644 --- a/src/frameworks/Accelerate/vecLib/vDSP/include/vDSP/vDSP.h +++ b/src/frameworks/Accelerate/vecLib/vDSP/include/vDSP/vDSP.h @@ -24,6 +24,51 @@ typedef unsigned long vDSP_Length; typedef long vDSP_Stride; +typedef struct DSPComplex { + float real; + float imag; +} DSPComplex; + +typedef struct DSPSplitComplex { + float *realp; + float *imagp; +} DSPSplitComplex; + +typedef int FFTDirection; +typedef int FFTRadix; + +enum { + kFFTDirection_Forward = 1, + kFFTDirection_Inverse = -1 +}; + +enum { + kFFTRadix2 = 0, + kFFTRadix3 = 1, + kFFTRadix5 = 2 +}; + +typedef struct OpaqueFFTSetup *FFTSetup; + +void vDSP_vadd(const float *__A, vDSP_Stride __IA, const float *__B, vDSP_Stride __IB, float *__C, vDSP_Stride __IC, vDSP_Length __N); +void vDSP_vsub(const float *__B, vDSP_Stride __IB, const float *__A, vDSP_Stride __IA, float *__C, vDSP_Stride __IC, vDSP_Length __N); +void vDSP_vmul(const float *__A, vDSP_Stride __IA, const float *__B, vDSP_Stride __IB, float *__C, vDSP_Stride __IC, vDSP_Length __N); +void vDSP_vsmul(const float *__A, vDSP_Stride __IA, const float *__B, float *__C, vDSP_Stride __IC, vDSP_Length __N); +void vDSP_vsadd(const float *__A, vDSP_Stride __IA, const float *__B, float *__C, vDSP_Stride __IC, vDSP_Length __N); +void vDSP_vsma(const float *__A, vDSP_Stride __IA, const float *__B, const float *__C, vDSP_Stride __IC, float *__D, vDSP_Stride __ID, vDSP_Length __N); +void vDSP_svesq(const float *__A, vDSP_Stride __IA, float *__C, vDSP_Length __N); +void vDSP_vclip(const float *__A, vDSP_Stride __IA, const float *__B, const float *__C, float *__D, vDSP_Stride __ID, vDSP_Length __N); +void vDSP_maxmgv(const float *__A, vDSP_Stride __IA, float *__C, vDSP_Length __N); +void vDSP_conv(const float *__A, vDSP_Stride __IA, const float *__F, vDSP_Stride __IF, float *__C, vDSP_Stride __IC, vDSP_Length __N, vDSP_Length __P); +void vDSP_ctoz(const DSPComplex *__C, vDSP_Stride __IC, const DSPSplitComplex *__Z, vDSP_Stride __IZ, vDSP_Length __N); +void vDSP_ztoc(const DSPSplitComplex *__Z, vDSP_Stride __IZ, DSPComplex *__C, vDSP_Stride __IC, vDSP_Length __N); +void vDSP_zvmul(const DSPSplitComplex *__A, vDSP_Stride __IA, const DSPSplitComplex *__B, vDSP_Stride __IB, const DSPSplitComplex *__C, vDSP_Stride __IC, vDSP_Length __N, int __Conjugate); +void vDSP_deq22(const float *__A, vDSP_Stride __IA, const float *__B, float *__C, vDSP_Stride __IC, vDSP_Length __N); +void vDSP_deq22D(const double *__A, vDSP_Stride __IA, const double *__B, double *__C, vDSP_Stride __IC, vDSP_Length __N); +FFTSetup vDSP_create_fftsetup(vDSP_Length __Log2n, FFTRadix __Radix); +void vDSP_destroy_fftsetup(FFTSetup __setup); +void vDSP_fft_zrip(FFTSetup __Setup, const DSPSplitComplex *__C, vDSP_Stride __IC, vDSP_Length __Log2N, FFTDirection __Direction); + void* vDSP_DCT_CreateSetup(void); void* vDSP_DCT_Execute(void); void* vDSP_DFT_CreateSetup(void); @@ -63,17 +108,11 @@ void* vDSP_biquadm_SetTargetsDouble(void); void* vDSP_biquadm_SetTargetsSingle(void); void* vDSP_blkman_window(void); void* vDSP_blkman_windowD(void); -void* vDSP_conv(void); void* vDSP_convD(void); -void* vDSP_create_fftsetup(void); void* vDSP_create_fftsetupD(void); -void* vDSP_ctoz(void); void* vDSP_ctozD(void); -void* vDSP_deq22(void); -void* vDSP_deq22D(void); void* vDSP_desamp(void); void* vDSP_desampD(void); -void* vDSP_destroy_fftsetup(void); void* vDSP_destroy_fftsetupD(void); void* vDSP_distancesq(void); void* vDSP_distancesqD(void); @@ -117,7 +156,6 @@ void* vDSP_fft_zop(void); void* vDSP_fft_zopD(void); void* vDSP_fft_zopt(void); void* vDSP_fft_zoptD(void); -void* vDSP_fft_zrip(void); void* vDSP_fft_zripD(void); void* vDSP_fft_zript(void); void* vDSP_fft_zriptD(void); @@ -194,7 +232,6 @@ void* vDSP_sve_svesq(void); void* vDSP_sve_svesqD(void); void* vDSP_svemg(void); void* vDSP_svemgD(void); -void* vDSP_svesq(void); void* vDSP_svesqD(void); void* vDSP_svs(void); void* vDSP_svsD(void); @@ -203,7 +240,6 @@ void* vDSP_vaamD(void); void* vDSP_vabs(void); void* vDSP_vabsD(void); void* vDSP_vabsi(void); -void* vDSP_vadd(void); void* vDSP_vaddD(void); void* vDSP_vaddi(void); void* vDSP_vaddsub(void); @@ -216,7 +252,6 @@ void* vDSP_vasm(void); void* vDSP_vasmD(void); void* vDSP_vavlin(void); void* vDSP_vavlinD(void); -void* vDSP_vclip(void); void* vDSP_vclipD(void); void* vDSP_vclipc(void); void* vDSP_vclipcD(void); @@ -316,7 +351,6 @@ void* vDSP_vmsa(void); void* vDSP_vmsaD(void); void* vDSP_vmsb(void); void* vDSP_vmsbD(void); -void* vDSP_vmul(void); void* vDSP_vmulD(void); void* vDSP_vnabs(void); void* vDSP_vnabsD(void); @@ -350,7 +384,6 @@ void* vDSP_vrsum(void); void* vDSP_vrsumD(void); void* vDSP_vrvrs(void); void* vDSP_vrvrsD(void); -void* vDSP_vsadd(void); void* vDSP_vsaddD(void); void* vDSP_vsaddi(void); void* vDSP_vsbm(void); @@ -364,7 +397,6 @@ void* vDSP_vsdivD(void); void* vDSP_vsdivi(void); void* vDSP_vsimps(void); void* vDSP_vsimpsD(void); -void* vDSP_vsma(void); void* vDSP_vsmaD(void); void* vDSP_vsmfix24(void); void* vDSP_vsmfixu24(void); @@ -374,7 +406,6 @@ void* vDSP_vsmsb(void); void* vDSP_vsmsbD(void); void* vDSP_vsmsma(void); void* vDSP_vsmsmaD(void); -void* vDSP_vsmul(void); void* vDSP_vsmulD(void); void* vDSP_vsort(void); void* vDSP_vsortD(void); @@ -385,7 +416,6 @@ void* vDSP_vsq(void); void* vDSP_vsqD(void); void* vDSP_vssq(void); void* vDSP_vssqD(void); -void* vDSP_vsub(void); void* vDSP_vsubD(void); void* vDSP_vswap(void); void* vDSP_vswapD(void); @@ -439,7 +469,6 @@ void* vDSP_zrvmul(void); void* vDSP_zrvmulD(void); void* vDSP_zrvsub(void); void* vDSP_zrvsubD(void); -void* vDSP_ztoc(void); void* vDSP_ztocD(void); void* vDSP_ztrans(void); void* vDSP_ztransD(void); @@ -467,7 +496,6 @@ void* vDSP_zvmmaa(void); void* vDSP_zvmmaaD(void); void* vDSP_zvmov(void); void* vDSP_zvmovD(void); -void* vDSP_zvmul(void); void* vDSP_zvmulD(void); void* vDSP_zvneg(void); void* vDSP_zvnegD(void); diff --git a/src/frameworks/Accelerate/vecLib/vDSP/src/arith.c b/src/frameworks/Accelerate/vecLib/vDSP/src/arith.c new file mode 100644 index 000000000..52550fb96 --- /dev/null +++ b/src/frameworks/Accelerate/vecLib/vDSP/src/arith.c @@ -0,0 +1,226 @@ +/* + This file is part of Darling. + + Copyright (C) 2026 Darling Developers + + Darling is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Darling is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Darling. If not, see . +*/ + +#include + +void vDSP_vadd(const float *__A, vDSP_Stride __IA, const float *__B, vDSP_Stride __IB, float *__C, vDSP_Stride __IC, vDSP_Length __N) +{ + if (__IA == 1 && __IB == 1 && __IC == 1) + { + for (vDSP_Length i = 0; i < __N; i++) + __C[i] = __A[i] + __B[i]; + } + else + { + for (vDSP_Length i = 0; i < __N; i++) + __C[i * __IC] = __A[i * __IA] + __B[i * __IB]; + } +} + +// Apple's argument order: the subtrahend B comes first, C = A - B. +void vDSP_vsub(const float *__B, vDSP_Stride __IB, const float *__A, vDSP_Stride __IA, float *__C, vDSP_Stride __IC, vDSP_Length __N) +{ + if (__IA == 1 && __IB == 1 && __IC == 1) + { + for (vDSP_Length i = 0; i < __N; i++) + __C[i] = __A[i] - __B[i]; + } + else + { + for (vDSP_Length i = 0; i < __N; i++) + __C[i * __IC] = __A[i * __IA] - __B[i * __IB]; + } +} + +void vDSP_vmul(const float *__A, vDSP_Stride __IA, const float *__B, vDSP_Stride __IB, float *__C, vDSP_Stride __IC, vDSP_Length __N) +{ + if (__IA == 1 && __IB == 1 && __IC == 1) + { + for (vDSP_Length i = 0; i < __N; i++) + __C[i] = __A[i] * __B[i]; + } + else + { + for (vDSP_Length i = 0; i < __N; i++) + __C[i * __IC] = __A[i * __IA] * __B[i * __IB]; + } +} + +void vDSP_vsmul(const float *__A, vDSP_Stride __IA, const float *__B, float *__C, vDSP_Stride __IC, vDSP_Length __N) +{ + const float b = *__B; + + if (__IA == 1 && __IC == 1) + { + for (vDSP_Length i = 0; i < __N; i++) + __C[i] = __A[i] * b; + } + else + { + for (vDSP_Length i = 0; i < __N; i++) + __C[i * __IC] = __A[i * __IA] * b; + } +} + +void vDSP_vsadd(const float *__A, vDSP_Stride __IA, const float *__B, float *__C, vDSP_Stride __IC, vDSP_Length __N) +{ + const float b = *__B; + + if (__IA == 1 && __IC == 1) + { + for (vDSP_Length i = 0; i < __N; i++) + __C[i] = __A[i] + b; + } + else + { + for (vDSP_Length i = 0; i < __N; i++) + __C[i * __IC] = __A[i * __IA] + b; + } +} + +void vDSP_vsma(const float *__A, vDSP_Stride __IA, const float *__B, const float *__C, vDSP_Stride __IC, float *__D, vDSP_Stride __ID, vDSP_Length __N) +{ + const float b = *__B; + + if (__IA == 1 && __IC == 1 && __ID == 1) + { + for (vDSP_Length i = 0; i < __N; i++) + __D[i] = __A[i] * b + __C[i]; + } + else + { + for (vDSP_Length i = 0; i < __N; i++) + __D[i * __ID] = __A[i * __IA] * b + __C[i * __IC]; + } +} + +void vDSP_svesq(const float *__A, vDSP_Stride __IA, float *__C, vDSP_Length __N) +{ + float sum = 0; + + for (vDSP_Length i = 0; i < __N; i++) + sum += __A[i * __IA] * __A[i * __IA]; + *__C = sum; +} + +void vDSP_vclip(const float *__A, vDSP_Stride __IA, const float *__B, const float *__C, float *__D, vDSP_Stride __ID, vDSP_Length __N) +{ + const float low = *__B, high = *__C; + + for (vDSP_Length i = 0; i < __N; i++) + { + float v = __A[i * __IA]; + if (v < low) + v = low; + else if (v > high) + v = high; + __D[i * __ID] = v; + } +} + +// Correlation, or convolution when F points at the filter's last tap with a negative stride. +void vDSP_conv(const float *__A, vDSP_Stride __IA, const float *__F, vDSP_Stride __IF, float *__C, vDSP_Stride __IC, vDSP_Length __N, vDSP_Length __P) +{ + for (vDSP_Length n = 0; n < __N; n++) + { + const float *a = __A + n * __IA; + float sum = 0; + + for (vDSP_Length p = 0; p < __P; p++) + sum += a[p * __IA] * __F[p * __IF]; + __C[n * __IC] = sum; + } +} + +// IC counts floats, so 2 is a contiguous DSPComplex array. +void vDSP_ctoz(const DSPComplex *__C, vDSP_Stride __IC, const DSPSplitComplex *__Z, vDSP_Stride __IZ, vDSP_Length __N) +{ + const float *c = (const float *) __C; + + for (vDSP_Length i = 0; i < __N; i++) + { + __Z->realp[i * __IZ] = c[i * __IC]; + __Z->imagp[i * __IZ] = c[i * __IC + 1]; + } +} + +void vDSP_ztoc(const DSPSplitComplex *__Z, vDSP_Stride __IZ, DSPComplex *__C, vDSP_Stride __IC, vDSP_Length __N) +{ + float *c = (float *) __C; + + for (vDSP_Length i = 0; i < __N; i++) + { + c[i * __IC] = __Z->realp[i * __IZ]; + c[i * __IC + 1] = __Z->imagp[i * __IZ]; + } +} + +// C may alias A or B. Conjugate is 1 for A * B and -1 for conj(A) * B. +void vDSP_zvmul(const DSPSplitComplex *__A, vDSP_Stride __IA, const DSPSplitComplex *__B, vDSP_Stride __IB, const DSPSplitComplex *__C, vDSP_Stride __IC, vDSP_Length __N, int __Conjugate) +{ + const float sign = __Conjugate == -1 ? -1.0f : 1.0f; + + for (vDSP_Length i = 0; i < __N; i++) + { + const float ar = __A->realp[i * __IA], ai = sign * __A->imagp[i * __IA]; + const float br = __B->realp[i * __IB], bi = __B->imagp[i * __IB]; + + __C->realp[i * __IC] = ar * br - ai * bi; + __C->imagp[i * __IC] = ar * bi + ai * br; + } +} + +// Two-pole two-zero filter. A and C hold N + 2 elements; C[0] and C[1] are the initial outputs. +void vDSP_deq22D(const double *__A, vDSP_Stride __IA, const double *__B, double *__C, vDSP_Stride __IC, vDSP_Length __N) +{ + const double b0 = __B[0], b1 = __B[1], b2 = __B[2], a1 = __B[3], a2 = __B[4]; + double x1 = __A[__IA], x2 = __A[0]; + double y1 = __C[__IC], y2 = __C[0]; + + for (vDSP_Length n = 2; n < __N + 2; n++) + { + const double x = __A[n * __IA]; + const double y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2; + + __C[n * __IC] = y; + x2 = x1; + x1 = x; + y2 = y1; + y1 = y; + } +} + +void vDSP_deq22(const float *__A, vDSP_Stride __IA, const float *__B, float *__C, vDSP_Stride __IC, vDSP_Length __N) +{ + const float b0 = __B[0], b1 = __B[1], b2 = __B[2], a1 = __B[3], a2 = __B[4]; + float x1 = __A[__IA], x2 = __A[0]; + float y1 = __C[__IC], y2 = __C[0]; + + for (vDSP_Length n = 2; n < __N + 2; n++) + { + const float x = __A[n * __IA]; + const float y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2; + + __C[n * __IC] = y; + x2 = x1; + x1 = x; + y2 = y1; + y1 = y; + } +} diff --git a/src/frameworks/Accelerate/vecLib/vDSP/src/basic.c b/src/frameworks/Accelerate/vecLib/vDSP/src/basic.c deleted file mode 100644 index ecdd15b23..000000000 --- a/src/frameworks/Accelerate/vecLib/vDSP/src/basic.c +++ /dev/null @@ -1,36 +0,0 @@ -/* - This file is part of Darling. - - Copyright (C) 2020 Lubos Dolezel - - Darling is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Darling is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Darling. If not, see . -*/ - -#include - -void vDSP_vsadd(const float *__A, vDSP_Stride __IA, const float *__B, float *__C, vDSP_Stride __IC, vDSP_Length __N) -{ - if (__IA == 1 && __IC == 1) - { - #pragma clang loop vectorize(enable) - for (vDSP_Length i = 0; i < __N; i++) - __C[i] = __A[i] + __B[i]; - } - else - { - #pragma clang loop vectorize(enable) - for (vDSP_Length i = 0; i < __N; i++) - __C[i * __IC] = __A[i * __IA] + __B[i * __IA]; - } -} diff --git a/src/frameworks/Accelerate/vecLib/vDSP/src/fft.c b/src/frameworks/Accelerate/vecLib/vDSP/src/fft.c new file mode 100644 index 000000000..92873d796 --- /dev/null +++ b/src/frameworks/Accelerate/vecLib/vDSP/src/fft.c @@ -0,0 +1,162 @@ +/* + This file is part of Darling. + + Copyright (C) 2026 Darling Developers + + Darling is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Darling is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Darling. If not, see . +*/ + +#include +#include +#include + +// Setups are shared between threads, so only read-only twiddles live here. +struct OpaqueFFTSetup +{ + vDSP_Length log2n; + float *cosine; + float *sine; +}; + +FFTSetup vDSP_create_fftsetup(vDSP_Length __Log2n, FFTRadix __Radix) +{ + if (__Radix != kFFTRadix2 || __Log2n > 30) + return NULL; + + const size_t n = (size_t) 1 << __Log2n; + const size_t half = n > 1 ? n / 2 : 1; + struct OpaqueFFTSetup *setup = malloc(sizeof(*setup) + 2 * half * sizeof(float)); + + if (!setup) + return NULL; + setup->log2n = __Log2n; + setup->cosine = (float *) (setup + 1); + setup->sine = setup->cosine + half; + for (size_t j = 0; j < half; j++) + { + const double angle = 2.0 * M_PI * (double) j / (double) n; + setup->cosine[j] = cos(angle); + setup->sine[j] = sin(angle); + } + return setup; +} + +void vDSP_destroy_fftsetup(FFTSetup __setup) +{ + free(__setup); +} + +// In-place radix-2 complex FFT of 2^log2m points, e^-i for forward and e^+i for inverse, unscaled. +static void complex_fft(const struct OpaqueFFTSetup *setup, float *re, float *im, vDSP_Stride stride, vDSP_Length log2m, int inverse) +{ + const size_t m = (size_t) 1 << log2m; + const size_t full = (size_t) 1 << setup->log2n; + + for (size_t i = 1, j = 0; i < m; i++) + { + size_t bit = m >> 1; + for (; j & bit; bit >>= 1) + j ^= bit; + j ^= bit; + if (i < j) + { + float t = re[i * stride]; + re[i * stride] = re[j * stride]; + re[j * stride] = t; + t = im[i * stride]; + im[i * stride] = im[j * stride]; + im[j * stride] = t; + } + } + + for (size_t len = 2; len <= m; len <<= 1) + { + const size_t half = len >> 1; + const size_t step = full / len; + + for (size_t base = 0; base < m; base += len) + { + for (size_t k = 0; k < half; k++) + { + const float wr = setup->cosine[k * step]; + const float wi = inverse ? setup->sine[k * step] : -setup->sine[k * step]; + const size_t a = (base + k) * stride, b = (base + k + half) * stride; + const float tr = re[b] * wr - im[b] * wi; + const float ti = re[b] * wi + im[b] * wr; + + re[b] = re[a] - tr; + im[b] = im[a] - ti; + re[a] += tr; + im[a] += ti; + } + } + } +} + +// Real FFT on N = 2^Log2N points packed as N/2 complex values (even samples real, odd imaginary). +// Forward returns twice the DFT with X[0] in realp[0] and X[N/2] in imagp[0]; inverse is unscaled, +// so a round trip multiplies by 2N, as on macOS. +void vDSP_fft_zrip(FFTSetup __Setup, const DSPSplitComplex *__C, vDSP_Stride __IC, vDSP_Length __Log2N, FFTDirection __Direction) +{ + if (!__Setup || __Log2N == 0 || __Log2N > __Setup->log2n) + return; + + float *re = __C->realp, *im = __C->imagp; + const vDSP_Stride s = __IC; + const size_t m = (size_t) 1 << (__Log2N - 1); + const size_t step = ((size_t) 1 << __Setup->log2n) >> __Log2N; + + if (__Direction != kFFTDirection_Inverse) + { + complex_fft(__Setup, re, im, s, __Log2N - 1, 0); + + const float z0r = re[0], z0i = im[0]; + re[0] = 2 * (z0r + z0i); + im[0] = 2 * (z0r - z0i); + + for (size_t k = 1; k <= m / 2; k++) + { + const size_t j = m - k; + const float c = __Setup->cosine[k * step], sn = __Setup->sine[k * step]; + const float ar = re[k * s], ai = im[k * s], br = re[j * s], bi = im[j * s]; + const float sr = ar + br, si = ai - bi, dr = ar - br, di = ai + bi; + + re[k * s] = sr + (c * di - sn * dr); + im[k * s] = si - (c * dr + sn * di); + re[j * s] = sr - (c * di - sn * dr); + im[j * s] = -si - (c * dr + sn * di); + } + } + else + { + const float p0r = re[0], p0i = im[0]; + re[0] = p0r + p0i; + im[0] = p0r - p0i; + + for (size_t k = 1; k <= m / 2; k++) + { + const size_t j = m - k; + const float c = __Setup->cosine[k * step], sn = __Setup->sine[k * step]; + const float ar = re[k * s], ai = im[k * s], br = re[j * s], bi = im[j * s]; + const float sr = ar + br, si = ai - bi, dr = ar - br, di = ai + bi; + + re[k * s] = sr - (c * di + sn * dr); + im[k * s] = si + (c * dr - sn * di); + re[j * s] = sr + (c * di + sn * dr); + im[j * s] = -si + (c * dr - sn * di); + } + + complex_fft(__Setup, re, im, s, __Log2N - 1, 1); + } +} diff --git a/src/frameworks/Accelerate/vecLib/vDSP/src/vDSP.c b/src/frameworks/Accelerate/vecLib/vDSP/src/vDSP.c index 24c037abc..cc18546ce 100644 --- a/src/frameworks/Accelerate/vecLib/vDSP/src/vDSP.c +++ b/src/frameworks/Accelerate/vecLib/vDSP/src/vDSP.c @@ -263,54 +263,24 @@ void* vDSP_blkman_windowD(void) return NULL; } -void* vDSP_conv(void) -{ - if (verbose) puts("STUB: vDSP_conv called"); - return NULL; -} - void* vDSP_convD(void) { if (verbose) puts("STUB: vDSP_convD called"); return NULL; } -void* vDSP_create_fftsetup(void) -{ - if (verbose) puts("STUB: vDSP_create_fftsetup called"); - return NULL; -} - void* vDSP_create_fftsetupD(void) { if (verbose) puts("STUB: vDSP_create_fftsetupD called"); return NULL; } -void* vDSP_ctoz(void) -{ - if (verbose) puts("STUB: vDSP_ctoz called"); - return NULL; -} - void* vDSP_ctozD(void) { if (verbose) puts("STUB: vDSP_ctozD called"); return NULL; } -void* vDSP_deq22(void) -{ - if (verbose) puts("STUB: vDSP_deq22 called"); - return NULL; -} - -void* vDSP_deq22D(void) -{ - if (verbose) puts("STUB: vDSP_deq22D called"); - return NULL; -} - void* vDSP_desamp(void) { if (verbose) puts("STUB: vDSP_desamp called"); @@ -323,12 +293,6 @@ void* vDSP_desampD(void) return NULL; } -void* vDSP_destroy_fftsetup(void) -{ - if (verbose) puts("STUB: vDSP_destroy_fftsetup called"); - return NULL; -} - void* vDSP_destroy_fftsetupD(void) { if (verbose) puts("STUB: vDSP_destroy_fftsetupD called"); @@ -587,12 +551,6 @@ void* vDSP_fft_zoptD(void) return NULL; } -void* vDSP_fft_zrip(void) -{ - if (verbose) puts("STUB: vDSP_fft_zrip called"); - return NULL; -} - void* vDSP_fft_zripD(void) { if (verbose) puts("STUB: vDSP_fft_zripD called"); @@ -1057,12 +1015,6 @@ void* vDSP_svemgD(void) return NULL; } -void* vDSP_svesq(void) -{ - if (verbose) puts("STUB: vDSP_svesq called"); - return NULL; -} - void* vDSP_svesqD(void) { if (verbose) puts("STUB: vDSP_svesqD called"); @@ -1111,12 +1063,6 @@ void* vDSP_vabsi(void) return NULL; } -void* vDSP_vadd(void) -{ - if (verbose) puts("STUB: vDSP_vadd called"); - return NULL; -} - void* vDSP_vaddD(void) { if (verbose) puts("STUB: vDSP_vaddD called"); @@ -1189,12 +1135,6 @@ void* vDSP_vavlinD(void) return NULL; } -void* vDSP_vclip(void) -{ - if (verbose) puts("STUB: vDSP_vclip called"); - return NULL; -} - void* vDSP_vclipD(void) { if (verbose) puts("STUB: vDSP_vclipD called"); @@ -1789,12 +1729,6 @@ void* vDSP_vmsbD(void) return NULL; } -void* vDSP_vmul(void) -{ - if (verbose) puts("STUB: vDSP_vmul called"); - return NULL; -} - void* vDSP_vmulD(void) { if (verbose) puts("STUB: vDSP_vmulD called"); @@ -1993,14 +1927,6 @@ void* vDSP_vrvrsD(void) return NULL; } -/* -void* vDSP_vsadd(void) -{ - if (verbose) puts("STUB: vDSP_vsadd called"); - return NULL; -} -*/ - void* vDSP_vsaddD(void) { if (verbose) puts("STUB: vDSP_vsaddD called"); @@ -2079,12 +2005,6 @@ void* vDSP_vsimpsD(void) return NULL; } -void* vDSP_vsma(void) -{ - if (verbose) puts("STUB: vDSP_vsma called"); - return NULL; -} - void* vDSP_vsmaD(void) { if (verbose) puts("STUB: vDSP_vsmaD called"); @@ -2139,12 +2059,6 @@ void* vDSP_vsmsmaD(void) return NULL; } -void* vDSP_vsmul(void) -{ - if (verbose) puts("STUB: vDSP_vsmul called"); - return NULL; -} - void* vDSP_vsmulD(void) { if (verbose) puts("STUB: vDSP_vsmulD called"); @@ -2205,12 +2119,6 @@ void* vDSP_vssqD(void) return NULL; } -void* vDSP_vsub(void) -{ - if (verbose) puts("STUB: vDSP_vsub called"); - return NULL; -} - void* vDSP_vsubD(void) { if (verbose) puts("STUB: vDSP_vsubD called"); @@ -2529,12 +2437,6 @@ void* vDSP_zrvsubD(void) return NULL; } -void* vDSP_ztoc(void) -{ - if (verbose) puts("STUB: vDSP_ztoc called"); - return NULL; -} - void* vDSP_ztocD(void) { if (verbose) puts("STUB: vDSP_ztocD called"); @@ -2697,12 +2599,6 @@ void* vDSP_zvmovD(void) return NULL; } -void* vDSP_zvmul(void) -{ - if (verbose) puts("STUB: vDSP_zvmul called"); - return NULL; -} - void* vDSP_zvmulD(void) { if (verbose) puts("STUB: vDSP_zvmulD called"); diff --git a/tools/shutdown-user.sh b/tools/shutdown-user.sh index 072a6e2..c804100 100755 --- a/tools/shutdown-user.sh +++ b/tools/shutdown-user.sh @@ -1,26 +1,40 @@ #!/bin/sh -if [ "$(whoami)" != "root" ] -then - echo "$0 must be run as root, invoking sudo" - exec sudo su -c "$0" "$@" +# Rootless installs own only this workspace/prefix, never all launchd users. +if [ -n "${DARLING_WORKSPACE:-}" ]; then + : "${DPREFIX:?rootless install requires an explicit prefix}" + exec python3 "$DARLING_WORKSPACE/scripts/nix-idle.py" +fi + +if [ "$(whoami)" != "root" ]; then + echo "$0 must be run as root, invoking sudo" + exec sudo sh "$0" "$@" fi echo "Seeing if Darling is currently running" -# Sometimes `pgrep launchd` doesn't work when `pgrep -f launchd` does. Not sure -# if we want to include arguments (and capture rare, but possible false -# positives). -PID=$(pgrep -f launchd) +running_pids() { + # Match the guest init itself, not commands mentioning launchd. Zombies + # cannot respond to shutdown and must be reaped by their parent. + ps -eo pid=,stat=,args= | awk '$2 !~ /^Z/ && $3 == "/sbin/launchd" { print $1 }' +} -while [ -n "$PID" ] -do - THISPID=$(echo $PID | head -n1) - RUNNING_USER=$(ps -o uname= -p $THISPID | head -n1) - echo "Darling currently running for $RUNNING_USER, shutting it down..." - su --login "$RUNNING_USER" -c "darling shutdown" - sleep 2 - PID=$(pgrep -f launchd) +attempts=0 +while :; do + PID=$(running_pids | head -n1) + [ -n "$PID" ] || break + if [ "$attempts" -ge 5 ]; then + echo "Darling launchd PID $PID remains alive after shutdown; refusing to continue installation. Check for an orphaned container or a custom DPREFIX." >&2 + exit 1 + fi + RUNNING_USER=$(ps -o uname= -p "$PID" | head -n1 | tr -d '[:space:]') + # The process may have exited between enumeration and the owner query. + if [ -n "$RUNNING_USER" ]; then + echo "Darling currently running for $RUNNING_USER, shutting it down..." + timeout 15s su --login "$RUNNING_USER" -c "darling shutdown" || true + fi + attempts=$((attempts + 1)) + sleep 2 done echo "No instances running now" diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -204,8 +204,9 @@ InstallSymlink(private/etc ${CMAKE_INSTALL_PREFIX}/libexec/darling/etc) InstallSymlink(private/var ${CMAKE_INSTALL_PREFIX}/libexec/darling/var) -install(FILES etc/resolv.conf - DESTINATION libexec/darling/private/etc) +# Follow the host resolver, including local DNS proxies and network changes. +# Prefix-local resolver overrides still take precedence over this base symlink. +InstallSymlink(/Volumes/SystemRoot/etc/resolv.conf ${CMAKE_INSTALL_PREFIX}/libexec/darling/private/etc/resolv.conf) InstallSymlink(/Volumes/SystemRoot/etc/machine-id ${CMAKE_INSTALL_PREFIX}/libexec/darling/private/etc/machine-id) InstallSymlink(/Volumes/SystemRoot/etc/nsswitch.conf ${CMAKE_INSTALL_PREFIX}/libexec/darling/private/etc/nsswitch.conf) @@ -236,7 +237,12 @@ install(DIRECTORY DESTINATION libexec/darling/Library/Preferences) if(NOT DEBIAN_PACKAGING) - install(CODE "execute_process(COMMAND bash ${DARLING_TOP_DIRECTORY}/tools/shutdown-user.sh)") + install(CODE " + execute_process(COMMAND bash \"${DARLING_TOP_DIRECTORY}/tools/shutdown-user.sh\" RESULT_VARIABLE DARLING_SHUTDOWN_RESULT) + if(NOT DARLING_SHUTDOWN_RESULT STREQUAL \"0\") + message(FATAL_ERROR \"Unable to stop old Darling containers; installation stopped\") + endif() + ") endif(NOT DEBIAN_PACKAGING) add_custom_target(uninstall diff --git a/src/shellspawn/shellspawn-fds.h b/src/shellspawn/shellspawn-fds.h new file mode 100644 index 0000000..1f3c473 --- /dev/null +++ b/src/shellspawn/shellspawn-fds.h @@ -0,0 +1,37 @@ +#ifndef SHELLSPAWN_FDS_H +#define SHELLSPAWN_FDS_H +#include +#include +#include + +// Preserve every source before replacing any standard descriptor: sources can +// themselves be 0, 1 or 2 when the daemon was launched with closed stdio. +static int shellspawn_remap_stdio(const int streams[3]) +{ + int saved[3] = { -1, -1, -1 }; + for (int i = 0; i < 3; ++i) { + saved[i] = fcntl(streams[i], F_DUPFD_CLOEXEC, 3); + if (saved[i] < 0) goto failure; + } + for (int i = 0; i < 3; ++i) + if (dup2(saved[i], i) < 0) goto failure; + for (int i = 0; i < 3; ++i) close(saved[i]); + return 0; +failure: { + int error = errno; + for (int i = 0; i < 3; ++i) if (saved[i] >= 0) close(saved[i]); + errno = error; + return -1; +}} + +static void shellspawn_close_streams(int streams[3]) +{ + for (int i = 0; i < 3; ++i) { + int fd = streams[i]; + streams[i] = -1; + for (int j = i + 1; j < 3; ++j) + if (streams[j] == fd) streams[j] = -1; + if (fd > STDERR_FILENO) close(fd); + } +} +#endif diff --git a/src/startup/mldr/elfcalls/threads.c b/src/startup/mldr/elfcalls/threads.c index e789986..0d56b4e 100644 --- a/src/startup/mldr/elfcalls/threads.c +++ b/src/startup/mldr/elfcalls/threads.c @@ -387,8 +387,9 @@ int __darling_thread_rpc_socket(void) { // this is the main thread t_server_socket = __dserver_main_thread_socket_fd; } else { - // threads should already have a per-thread socket assigned when they're created - abort(); + // Native host-library threads do not have a Darling RPC connection. + // Let the caller report EPIPE, including the resize-signal bridge. + return -1; } } return t_server_socket; diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPAInput.cpp b/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPAInput.cpp index c1fd1d402..777ca7c38 100644 --- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPAInput.cpp +++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPAInput.cpp @@ -31,15 +31,18 @@ OSStatus AudioHardwareImplPAInput::getPropertyData(const AudioObjectPropertyAddr { if (inAddress->mSelector == kAudioDevicePropertyStreamConfiguration) { + // A capture device has its channels on the input scope only. Reporting them on the + // output scope (copied from the output device) made capture enumerators such as + // Vivox and Chromium see a playback-only device with no input channels. size_t size = sizeof(AudioBufferList); - if (inAddress->mScope == kAudioDevicePropertyScopeOutput) + if (inAddress->mScope == kAudioDevicePropertyScopeInput) size += 1* sizeof(AudioBuffer); // Number of returned buffers is the number of channels if (AudioBufferList* abl = static_cast(outData); abl && *ioDataSize >= size) { - if (inAddress->mScope == kAudioDevicePropertyScopeOutput) + if (inAddress->mScope == kAudioDevicePropertyScopeInput) { abl->mNumberBuffers = 1; abl->mBuffers[0].mNumberChannels = 2; diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.h b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.h index 87db50d12..01a7b2a3e 100644 --- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.h +++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.h @@ -21,6 +21,8 @@ along with Darling. If not, see . #define AUDIOHARDWARESTREAMPAINPUT_H #include "AudioHardwareStreamPA.h" #include +#include +#include class AudioHardwareStreamPAInput : public AudioHardwareStreamPA { @@ -30,6 +32,12 @@ private: void start() override; static void paStreamReadCB(pa_stream* s, size_t length, void* self); + // Only touched on the PA queue. + uint64_t m_frames = 0; + uint64_t m_lastInputHost = 0; + size_t m_blockBytes = 0; + std::vector m_scratch; + // std::ofstream m_dump; };